@multiplayer-app/session-recorder-react-native 0.0.1 → 1.0.0

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 (228) hide show
  1. package/README.md +708 -83
  2. package/SessionRecorderNative.podspec +26 -0
  3. package/android/build.gradle +34 -0
  4. package/copy-react-native-dist.sh +34 -16
  5. package/dist/components/ScreenRecorderView/ScreenRecorderView.js +1 -1
  6. package/dist/components/ScreenRecorderView/ScreenRecorderView.js.map +1 -1
  7. package/dist/components/SessionRecorderWidget/ErrorBanner.d.ts +7 -0
  8. package/dist/components/SessionRecorderWidget/ErrorBanner.js +1 -0
  9. package/dist/components/SessionRecorderWidget/ErrorBanner.js.map +1 -0
  10. package/dist/components/SessionRecorderWidget/FinalPopover.d.ts +12 -0
  11. package/dist/components/SessionRecorderWidget/FinalPopover.js +1 -0
  12. package/dist/components/SessionRecorderWidget/FinalPopover.js.map +1 -0
  13. package/dist/components/SessionRecorderWidget/FloatingButton.d.ts +8 -0
  14. package/dist/components/SessionRecorderWidget/FloatingButton.js +1 -0
  15. package/dist/components/SessionRecorderWidget/FloatingButton.js.map +1 -0
  16. package/dist/components/SessionRecorderWidget/InitialPopover.d.ts +16 -0
  17. package/dist/components/SessionRecorderWidget/InitialPopover.js +1 -0
  18. package/dist/components/SessionRecorderWidget/InitialPopover.js.map +1 -0
  19. package/dist/components/SessionRecorderWidget/ModalContainer.d.ts +8 -0
  20. package/dist/components/SessionRecorderWidget/ModalContainer.js +1 -0
  21. package/dist/components/SessionRecorderWidget/ModalContainer.js.map +1 -0
  22. package/dist/components/SessionRecorderWidget/ModalHeader.d.ts +6 -0
  23. package/dist/components/SessionRecorderWidget/ModalHeader.js +1 -0
  24. package/dist/components/SessionRecorderWidget/ModalHeader.js.map +1 -0
  25. package/dist/components/SessionRecorderWidget/SessionRecorderWidget.d.ts +5 -0
  26. package/dist/components/SessionRecorderWidget/SessionRecorderWidget.js +1 -0
  27. package/dist/components/SessionRecorderWidget/SessionRecorderWidget.js.map +1 -0
  28. package/dist/components/SessionRecorderWidget/icons.d.ts +11 -0
  29. package/dist/components/SessionRecorderWidget/icons.js +1 -0
  30. package/dist/components/SessionRecorderWidget/icons.js.map +1 -0
  31. package/dist/components/SessionRecorderWidget/index.d.ts +2 -0
  32. package/dist/components/SessionRecorderWidget/index.js +1 -0
  33. package/dist/components/SessionRecorderWidget/index.js.map +1 -0
  34. package/dist/components/SessionRecorderWidget/styles.d.ts +165 -0
  35. package/dist/components/SessionRecorderWidget/styles.js +1 -0
  36. package/dist/components/SessionRecorderWidget/styles.js.map +1 -0
  37. package/dist/components/index.d.ts +2 -1
  38. package/dist/components/index.js +1 -1
  39. package/dist/components/index.js.map +1 -1
  40. package/dist/config/constants.js +1 -1
  41. package/dist/config/constants.js.map +1 -1
  42. package/dist/config/defaults.d.ts +4 -4
  43. package/dist/config/defaults.js +1 -1
  44. package/dist/config/defaults.js.map +1 -1
  45. package/dist/config/masking.d.ts +2 -2
  46. package/dist/config/masking.js +1 -1
  47. package/dist/config/masking.js.map +1 -1
  48. package/dist/config/session-recorder.js +1 -1
  49. package/dist/config/session-recorder.js.map +1 -1
  50. package/dist/config/validators.d.ts +1 -1
  51. package/dist/config/validators.js +1 -1
  52. package/dist/config/validators.js.map +1 -1
  53. package/dist/config/widget.d.ts +9 -0
  54. package/dist/config/widget.js +1 -0
  55. package/dist/config/widget.js.map +1 -0
  56. package/dist/context/SessionRecorderContext.d.ts +12 -3
  57. package/dist/context/SessionRecorderContext.js +1 -1
  58. package/dist/context/SessionRecorderContext.js.map +1 -1
  59. package/dist/context/SessionRecorderStore.d.ts +12 -0
  60. package/dist/context/SessionRecorderStore.js +1 -0
  61. package/dist/context/SessionRecorderStore.js.map +1 -0
  62. package/dist/context/useSessionRecorderStore.d.ts +8 -0
  63. package/dist/context/useSessionRecorderStore.js +1 -0
  64. package/dist/context/useSessionRecorderStore.js.map +1 -0
  65. package/dist/context/useStoreSelector.d.ts +4 -0
  66. package/dist/context/useStoreSelector.js +1 -0
  67. package/dist/context/useStoreSelector.js.map +1 -0
  68. package/dist/index.d.ts +1 -1
  69. package/dist/index.js +1 -1
  70. package/dist/index.js.map +1 -1
  71. package/dist/native/GestureRecorderNative.d.ts +57 -0
  72. package/dist/native/GestureRecorderNative.js +1 -0
  73. package/dist/native/GestureRecorderNative.js.map +1 -0
  74. package/dist/native/SessionRecorderNative.d.ts +33 -0
  75. package/dist/native/SessionRecorderNative.js +1 -0
  76. package/dist/native/SessionRecorderNative.js.map +1 -0
  77. package/dist/native/index.d.ts +2 -0
  78. package/dist/native/index.js +1 -0
  79. package/dist/native/index.js.map +1 -0
  80. package/dist/otel/index.js +1 -1
  81. package/dist/otel/index.js.map +1 -1
  82. package/dist/patch/xhr.js +1 -1
  83. package/dist/patch/xhr.js.map +1 -1
  84. package/dist/recorder/eventExporter.d.ts +4 -1
  85. package/dist/recorder/eventExporter.js +1 -1
  86. package/dist/recorder/eventExporter.js.map +1 -1
  87. package/dist/recorder/gestureRecorder.d.ts +28 -62
  88. package/dist/recorder/gestureRecorder.js +1 -1
  89. package/dist/recorder/gestureRecorder.js.map +1 -1
  90. package/dist/recorder/index.d.ts +2 -0
  91. package/dist/recorder/index.js +1 -1
  92. package/dist/recorder/index.js.map +1 -1
  93. package/dist/recorder/navigationTracker.d.ts +4 -19
  94. package/dist/recorder/navigationTracker.js +1 -1
  95. package/dist/recorder/navigationTracker.js.map +1 -1
  96. package/dist/recorder/screenRecorder.d.ts +11 -5
  97. package/dist/recorder/screenRecorder.js +1 -1
  98. package/dist/recorder/screenRecorder.js.map +1 -1
  99. package/dist/services/api.service.d.ts +12 -3
  100. package/dist/services/api.service.js +1 -1
  101. package/dist/services/api.service.js.map +1 -1
  102. package/dist/services/network.service.d.ts +46 -0
  103. package/dist/services/network.service.js +1 -0
  104. package/dist/services/network.service.js.map +1 -0
  105. package/dist/services/screenMaskingService.d.ts +47 -0
  106. package/dist/services/screenMaskingService.js +1 -0
  107. package/dist/services/screenMaskingService.js.map +1 -0
  108. package/dist/services/storage.service.d.ts +18 -2
  109. package/dist/services/storage.service.js +1 -1
  110. package/dist/services/storage.service.js.map +1 -1
  111. package/dist/session-recorder.d.ts +18 -33
  112. package/dist/session-recorder.js +1 -1
  113. package/dist/session-recorder.js.map +1 -1
  114. package/dist/types/configs.d.ts +85 -0
  115. package/dist/types/configs.js +1 -0
  116. package/dist/types/configs.js.map +1 -0
  117. package/dist/types/index.d.ts +1 -0
  118. package/dist/types/index.js +1 -1
  119. package/dist/types/index.js.map +1 -1
  120. package/dist/types/session-recorder.d.ts +105 -132
  121. package/dist/types/session-recorder.js +1 -1
  122. package/dist/types/session-recorder.js.map +1 -1
  123. package/dist/utils/constants.optional.d.ts +21 -0
  124. package/dist/utils/constants.optional.expo.d.ts +3 -0
  125. package/dist/utils/constants.optional.expo.js +1 -0
  126. package/dist/utils/constants.optional.expo.js.map +1 -0
  127. package/dist/utils/constants.optional.js +1 -0
  128. package/dist/utils/constants.optional.js.map +1 -0
  129. package/dist/utils/createStore.d.ts +8 -0
  130. package/dist/utils/createStore.js +1 -0
  131. package/dist/utils/createStore.js.map +1 -0
  132. package/dist/utils/logger.d.ts +2 -7
  133. package/dist/utils/logger.js +1 -1
  134. package/dist/utils/logger.js.map +1 -1
  135. package/dist/utils/platform.d.ts +11 -0
  136. package/dist/utils/platform.js +1 -1
  137. package/dist/utils/platform.js.map +1 -1
  138. package/dist/utils/rrweb-events.d.ts +4 -3
  139. package/dist/utils/rrweb-events.js +1 -1
  140. package/dist/utils/rrweb-events.js.map +1 -1
  141. package/dist/utils/session.d.ts +2 -1
  142. package/dist/utils/session.js +1 -1
  143. package/dist/utils/session.js.map +1 -1
  144. package/dist/utils/shallowEqual.d.ts +1 -0
  145. package/dist/utils/shallowEqual.js +1 -0
  146. package/dist/utils/shallowEqual.js.map +1 -0
  147. package/dist/version.d.ts +1 -1
  148. package/dist/version.js +1 -1
  149. package/ios/GestureRecorderNative.m +21 -0
  150. package/ios/GestureRecorderNative.swift +316 -0
  151. package/ios/SessionRecorderNative.m +17 -0
  152. package/ios/SessionRecorderNative.podspec +26 -0
  153. package/ios/SessionRecorderNative.swift +599 -0
  154. package/package.json +15 -16
  155. package/react-native.config.js +12 -0
  156. package/RRWEB_INTEGRATION.md +0 -336
  157. package/VIEWSHOT_INTEGRATION_TEST.md +0 -123
  158. package/babel.config.js +0 -13
  159. package/dist/components/GestureCaptureWrapper/GestureCaptureWrapper.d.ts +0 -6
  160. package/dist/components/GestureCaptureWrapper/GestureCaptureWrapper.js +0 -1
  161. package/dist/components/GestureCaptureWrapper/GestureCaptureWrapper.js.map +0 -1
  162. package/dist/components/GestureCaptureWrapper/index.d.ts +0 -1
  163. package/dist/components/GestureCaptureWrapper/index.js +0 -1
  164. package/dist/components/GestureCaptureWrapper/index.js.map +0 -1
  165. package/dist/components/GestureCaptureWrapper.d.ts +0 -6
  166. package/dist/components/GestureCaptureWrapper.js +0 -1
  167. package/dist/components/GestureCaptureWrapper.js.map +0 -1
  168. package/dist/expo.d.ts +0 -7
  169. package/dist/expo.js +0 -1
  170. package/dist/expo.js.map +0 -1
  171. package/dist/otel/instrumentations/gestureInstrumentation.d.ts +0 -15
  172. package/dist/otel/instrumentations/gestureInstrumentation.js +0 -1
  173. package/dist/otel/instrumentations/gestureInstrumentation.js.map +0 -1
  174. package/dist/otel/instrumentations/reactNativeInstrumentation.d.ts +0 -8
  175. package/dist/otel/instrumentations/reactNativeInstrumentation.js +0 -1
  176. package/dist/otel/instrumentations/reactNativeInstrumentation.js.map +0 -1
  177. package/dist/otel/instrumentations/reactNavigationInstrumentation.d.ts +0 -13
  178. package/dist/otel/instrumentations/reactNavigationInstrumentation.js +0 -1
  179. package/dist/otel/instrumentations/reactNavigationInstrumentation.js.map +0 -1
  180. package/dist/recorder/gestureHandlerRecorder.d.ts +0 -19
  181. package/dist/recorder/gestureHandlerRecorder.js +0 -1
  182. package/dist/recorder/gestureHandlerRecorder.js.map +0 -1
  183. package/dist/types/rrweb.d.ts +0 -118
  184. package/dist/types/rrweb.js +0 -1
  185. package/dist/types/rrweb.js.map +0 -1
  186. package/scripts/generate-app-metadata.js +0 -173
  187. package/src/components/GestureCaptureWrapper/GestureCaptureWrapper.tsx +0 -86
  188. package/src/components/GestureCaptureWrapper/index.ts +0 -1
  189. package/src/components/ScreenRecorderView/ScreenRecorderView.tsx +0 -72
  190. package/src/components/ScreenRecorderView/index.ts +0 -1
  191. package/src/components/index.ts +0 -1
  192. package/src/config/constants.ts +0 -60
  193. package/src/config/defaults.ts +0 -82
  194. package/src/config/index.ts +0 -6
  195. package/src/config/masking.ts +0 -27
  196. package/src/config/session-recorder.ts +0 -55
  197. package/src/config/validators.ts +0 -31
  198. package/src/context/SessionRecorderContext.tsx +0 -75
  199. package/src/expo.ts +0 -11
  200. package/src/index.ts +0 -17
  201. package/src/otel/helpers.ts +0 -275
  202. package/src/otel/index.ts +0 -138
  203. package/src/otel/instrumentations/index.ts +0 -115
  204. package/src/patch/index.ts +0 -1
  205. package/src/patch/xhr.ts +0 -142
  206. package/src/recorder/eventExporter.ts +0 -141
  207. package/src/recorder/gestureRecorder.ts +0 -498
  208. package/src/recorder/index.ts +0 -179
  209. package/src/recorder/navigationTracker.ts +0 -449
  210. package/src/recorder/screenRecorder.ts +0 -498
  211. package/src/services/api.service.ts +0 -203
  212. package/src/services/storage.service.ts +0 -158
  213. package/src/session-recorder.ts +0 -600
  214. package/src/types/expo.d.ts +0 -23
  215. package/src/types/index.ts +0 -28
  216. package/src/types/session-recorder.ts +0 -423
  217. package/src/types/session.ts +0 -65
  218. package/src/utils/app-metadata.ts +0 -31
  219. package/src/utils/index.ts +0 -8
  220. package/src/utils/logger.ts +0 -225
  221. package/src/utils/platform.ts +0 -384
  222. package/src/utils/request-utils.ts +0 -61
  223. package/src/utils/rrweb-events.ts +0 -309
  224. package/src/utils/session.ts +0 -18
  225. package/src/utils/time.ts +0 -17
  226. package/src/utils/type-utils.ts +0 -75
  227. package/src/version.ts +0 -1
  228. package/tsconfig.json +0 -24
@@ -0,0 +1,599 @@
1
+ import UIKit
2
+ import React
3
+ import WebKit
4
+
5
+ @objc(SessionRecorderNative)
6
+ class SessionRecorderNative: NSObject {
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
+
47
+ @objc func captureAndMask(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
48
+ DispatchQueue.main.async {
49
+ guard let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) else {
50
+ reject("NO_WINDOW", "Unable to get key window", nil)
51
+ return
52
+ }
53
+
54
+ UIGraphicsBeginImageContextWithOptions(window.bounds.size, false, UIScreen.main.scale)
55
+ window.drawHierarchy(in: window.bounds, afterScreenUpdates: true)
56
+ let screenshot = UIGraphicsGetImageFromCurrentImageContext()
57
+ UIGraphicsEndImageContext()
58
+
59
+ guard let image = screenshot else {
60
+ reject("CAPTURE_FAILED", "Failed to capture screen", nil)
61
+ return
62
+ }
63
+
64
+ // Apply masking to sensitive elements
65
+ let maskedImage = self.applyMasking(to: image, in: window)
66
+
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)
76
+ } else {
77
+ reject("ENCODING_FAILED", "Failed to encode image", nil)
78
+ }
79
+ }
80
+ }
81
+
82
+ @objc func captureAndMaskWithOptions(_ options: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
83
+ DispatchQueue.main.async {
84
+ // Update configuration from options
85
+ self.updateConfiguration(from: options)
86
+
87
+ guard let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) else {
88
+ reject("NO_WINDOW", "Unable to get key window", nil)
89
+ return
90
+ }
91
+
92
+ UIGraphicsBeginImageContextWithOptions(window.bounds.size, false, UIScreen.main.scale)
93
+ window.drawHierarchy(in: window.bounds, afterScreenUpdates: true)
94
+ let screenshot = UIGraphicsGetImageFromCurrentImageContext()
95
+ UIGraphicsEndImageContext()
96
+
97
+ guard let image = screenshot else {
98
+ reject("CAPTURE_FAILED", "Failed to capture screen", nil)
99
+ return
100
+ }
101
+
102
+ // Apply masking with custom options
103
+ let maskedImage = self.applyMaskingWithOptions(to: image, in: window, options: options)
104
+
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) {
112
+ let base64 = data.base64EncodedString()
113
+ resolve(base64)
114
+ } else {
115
+ reject("ENCODING_FAILED", "Failed to encode image", nil)
116
+ }
117
+ }
118
+ }
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
+
156
+ private func applyMasking(to image: UIImage, in window: UIWindow) -> UIImage {
157
+ return applyMaskingWithOptions(to: image, in: window, options: [:])
158
+ }
159
+
160
+ private func applyMaskingWithOptions(to image: UIImage, in window: UIWindow, options: NSDictionary) -> UIImage {
161
+ UIGraphicsBeginImageContextWithOptions(image.size, false, image.scale)
162
+ guard let context = UIGraphicsGetCurrentContext() else { return image }
163
+
164
+ // Draw the original image
165
+ image.draw(in: CGRect(origin: .zero, size: image.size))
166
+
167
+
168
+ var maskableWidgets: [CGRect] = []
169
+ var maskChildren = false
170
+ findMaskableWidgets(window, window, &maskableWidgets, &maskChildren)
171
+
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
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)
187
+ }
188
+
189
+ let maskedImage = UIGraphicsGetImageFromCurrentImageContext() ?? image
190
+ UIGraphicsEndImageContext()
191
+
192
+ return maskedImage
193
+ }
194
+
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
+ }
287
+
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
293
+ }
294
+ }
295
+
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
311
+ }
312
+ }
313
+
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
340
+ }
341
+
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 {
382
+ return true
383
+ } else {
384
+ // if there's no text, there's nothing to mask
385
+ return false
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 }
398
+
399
+ // sensitive, regardless
400
+ if view.isNoCapture() {
401
+ return true
402
+ }
403
+
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
415
+ }
416
+
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
421
+ }
422
+
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)
434
+ context.fill(frame)
435
+
436
+ // Add subtle border for visual definition
437
+ context.setStrokeColor(UIColor.systemGray4.cgColor)
438
+ context.setLineWidth(0.5)
439
+ context.stroke(frame)
440
+ }
441
+
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)
453
+ context.fill(frame)
454
+
455
+ // Add subtle border for visual definition
456
+ context.setStrokeColor(UIColor.systemGray4.cgColor)
457
+ context.setLineWidth(1.0)
458
+ context.stroke(frame)
459
+ }
460
+
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
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)
477
+ }
478
+
479
+ private func applyPixelateMask(in context: CGContext, frame: CGRect, image: UIImage) {
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
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
511
+ }
512
+ }
513
+
514
+ private enum MaskingType {
515
+ case blur
516
+ case rectangle
517
+ case pixelate
518
+ case none
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@multiplayer-app/session-recorder-react-native",
3
- "version": "0.0.1",
3
+ "version": "1.0.0",
4
4
  "description": "Multiplayer Fullstack Session Recorder for React Native",
5
5
  "author": {
6
6
  "name": "Multiplayer Software, Inc.",
@@ -20,10 +20,6 @@
20
20
  ".": {
21
21
  "import": "./dist/index.js",
22
22
  "require": "./dist/index.js"
23
- },
24
- "./expo": {
25
- "import": "./dist/expo.js",
26
- "require": "./dist/expo.js"
27
23
  }
28
24
  },
29
25
  "engines": {
@@ -43,10 +39,9 @@
43
39
  ],
44
40
  "scripts": {
45
41
  "clean": "rimraf dist",
42
+ "prebuild": "node -p \"'export const version = ' + JSON.stringify(require('./package.json').version)\" > src/version.ts",
46
43
  "prepublishOnly": "npm run build",
47
- "prebuild": "node -p \"'export const version = ' + JSON.stringify(require('./package.json').version)\" > src/version.ts && node scripts/generate-app-metadata.js",
48
- "build": "tsc && babel ./dist --out-dir dist --extensions '.js' && ./copy-react-native-dist.sh",
49
- "generate-metadata": "node scripts/generate-app-metadata.js"
44
+ "build": "tsc && babel ./dist --out-dir dist --extensions '.js' && ./copy-react-native-dist.sh"
50
45
  },
51
46
  "devDependencies": {
52
47
  "@babel/cli": "^7.19.3",
@@ -60,6 +55,8 @@
60
55
  "@types/react": "^18.2.0",
61
56
  "@types/react-native": "^0.72.0",
62
57
  "eslint": "8.48.0",
58
+ "react-native-safe-area-context": "^5.6.0",
59
+ "react-native-svg": "^15.12.0",
63
60
  "rimraf": "^5.0.5",
64
61
  "typescript": "5.7.3"
65
62
  },
@@ -79,19 +76,21 @@
79
76
  "@react-native-community/netinfo": "^11.1.0",
80
77
  "@rrweb/types": "^2.0.0-alpha.18",
81
78
  "lib0": "0.2.82",
82
- "react-native-gesture-handler": "^2.14.0",
83
- "react-native-mmkv": "^2.11.0",
84
- "react-native-reanimated": "^3.6.0",
79
+ "react-native-reanimated": "^4.1.1",
85
80
  "react-native-view-shot": "^4.0.3",
86
81
  "socket.io-client": "4.7.5"
87
82
  },
88
83
  "peerDependencies": {
89
84
  "@opentelemetry/api": "^1.9.0",
90
- "expo": ">=49.0.0 <54.0.0",
85
+ "expo-constants": "*",
91
86
  "react": ">=18.0.0 <20.0.0",
92
- "react-native": ">=0.72.0 <0.82.0"
87
+ "react-native": ">=0.72.0 <0.83.0",
88
+ "react-native-safe-area-context": ">=4.0.0 <6.0.0 || ^5.0.0",
89
+ "react-native-svg": ">=15.12.0 <16.0.0"
93
90
  },
94
- "optionalDependencies": {
95
- "expo-constants": "^15.0.0"
91
+ "peerDependenciesMeta": {
92
+ "expo-constants": {
93
+ "optional": true
94
+ }
96
95
  }
97
- }
96
+ }
@@ -0,0 +1,12 @@
1
+ module.exports = {
2
+ dependency: {
3
+ platforms: {
4
+ android: {
5
+ sourceDir: './android'
6
+ },
7
+ ios: {
8
+ podspecPath: './ios/SessionRecorderNative.podspec'
9
+ }
10
+ }
11
+ }
12
+ }