@multiplayer-app/session-recorder-react-native 0.0.1-beta.4 → 0.0.1-beta.6

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 (94) hide show
  1. package/android/build.gradle +32 -0
  2. package/android/src/main/AndroidManifest.xml +2 -0
  3. package/android/src/main/java/com/multiplayer/sessionrecorder/ScreenMaskingModule.kt +202 -0
  4. package/android/src/main/java/com/multiplayer/sessionrecorder/ScreenMaskingPackage.kt +16 -0
  5. package/android/src/main/java/com/multiplayer/sessionrecorder/SessionRecorderModule.kt +202 -0
  6. package/android/src/main/java/com/multiplayer/sessionrecorder/SessionRecorderPackage.kt +16 -0
  7. package/dist/components/MaskableComponent.d.ts +22 -0
  8. package/dist/components/MaskableComponent.js +1 -0
  9. package/dist/components/MaskableComponent.js.map +1 -0
  10. package/dist/components/MaskableTextInput.d.ts +14 -0
  11. package/dist/components/MaskableTextInput.js +1 -0
  12. package/dist/components/MaskableTextInput.js.map +1 -0
  13. package/dist/components/SessionRecorderWidget/FinalPopover.js +1 -1
  14. package/dist/components/SessionRecorderWidget/FinalPopover.js.map +1 -1
  15. package/dist/components/SessionRecorderWidget/FloatingButton.js +1 -1
  16. package/dist/components/SessionRecorderWidget/FloatingButton.js.map +1 -1
  17. package/dist/components/SessionRecorderWidget/InitialPopover.js +1 -1
  18. package/dist/components/SessionRecorderWidget/InitialPopover.js.map +1 -1
  19. package/dist/components/SessionRecorderWidget/ModalContainer.js +1 -1
  20. package/dist/components/SessionRecorderWidget/ModalContainer.js.map +1 -1
  21. package/dist/components/SessionRecorderWidget/ModalHeader.d.ts +6 -0
  22. package/dist/components/SessionRecorderWidget/ModalHeader.js +1 -0
  23. package/dist/components/SessionRecorderWidget/ModalHeader.js.map +1 -0
  24. package/dist/components/SessionRecorderWidget/icons.d.ts +1 -0
  25. package/dist/components/SessionRecorderWidget/icons.js +1 -1
  26. package/dist/components/SessionRecorderWidget/icons.js.map +1 -1
  27. package/dist/components/SessionRecorderWidget/styles.d.ts +39 -22
  28. package/dist/components/SessionRecorderWidget/styles.js +1 -1
  29. package/dist/components/SessionRecorderWidget/styles.js.map +1 -1
  30. package/dist/components/index.d.ts +2 -0
  31. package/dist/components/index.js +1 -1
  32. package/dist/components/index.js.map +1 -1
  33. package/dist/config/defaults.js +1 -1
  34. package/dist/config/defaults.js.map +1 -1
  35. package/dist/config/masking.js +1 -1
  36. package/dist/config/masking.js.map +1 -1
  37. package/dist/native/ScreenMasking.d.ts +21 -0
  38. package/dist/native/ScreenMasking.js +1 -0
  39. package/dist/native/ScreenMasking.js.map +1 -0
  40. package/dist/native/SessionRecorderNative.d.ts +21 -0
  41. package/dist/native/SessionRecorderNative.js +1 -0
  42. package/dist/native/SessionRecorderNative.js.map +1 -0
  43. package/dist/recorder/screenRecorder.d.ts +1 -0
  44. package/dist/recorder/screenRecorder.js +1 -1
  45. package/dist/recorder/screenRecorder.js.map +1 -1
  46. package/dist/recorder/screenshotManager.d.ts +10 -0
  47. package/dist/recorder/screenshotManager.js +1 -0
  48. package/dist/recorder/screenshotManager.js.map +1 -0
  49. package/dist/services/screenMaskingService.d.ts +39 -0
  50. package/dist/services/screenMaskingService.js +1 -0
  51. package/dist/services/screenMaskingService.js.map +1 -0
  52. package/dist/types/session-recorder.d.ts +6 -0
  53. package/dist/types/session-recorder.js.map +1 -1
  54. package/dist/utils/componentRegistry.d.ts +64 -0
  55. package/dist/utils/componentRegistry.js +1 -0
  56. package/dist/utils/componentRegistry.js.map +1 -0
  57. package/dist/utils/nativeModuleTest.d.ts +8 -0
  58. package/dist/utils/nativeModuleTest.js +1 -0
  59. package/dist/utils/nativeModuleTest.js.map +1 -0
  60. package/dist/utils/reactNativeHierarchyExtractor.d.ts +38 -0
  61. package/dist/utils/reactNativeHierarchyExtractor.js +1 -0
  62. package/dist/utils/reactNativeHierarchyExtractor.js.map +1 -0
  63. package/dist/utils/screenshotMasker.d.ts +96 -0
  64. package/dist/utils/screenshotMasker.js +1 -0
  65. package/dist/utils/screenshotMasker.js.map +1 -0
  66. package/dist/utils/viewHierarchyTracker.d.ts +89 -0
  67. package/dist/utils/viewHierarchyTracker.js +1 -0
  68. package/dist/utils/viewHierarchyTracker.js.map +1 -0
  69. package/docs/TROUBLESHOOTING.md +168 -0
  70. package/ios/ScreenMasking.m +12 -0
  71. package/ios/ScreenMasking.podspec +21 -0
  72. package/ios/ScreenMasking.swift +205 -0
  73. package/ios/SessionRecorder.m +12 -0
  74. package/ios/SessionRecorder.podspec +21 -0
  75. package/ios/SessionRecorder.swift +205 -0
  76. package/ios/SessionRecorderNative.podspec +21 -0
  77. package/package.json +10 -1
  78. package/react-native.config.js +15 -0
  79. package/src/components/SessionRecorderWidget/FinalPopover.tsx +5 -16
  80. package/src/components/SessionRecorderWidget/FloatingButton.tsx +14 -27
  81. package/src/components/SessionRecorderWidget/InitialPopover.tsx +3 -9
  82. package/src/components/SessionRecorderWidget/ModalContainer.tsx +77 -29
  83. package/src/components/SessionRecorderWidget/ModalHeader.tsx +24 -0
  84. package/src/components/SessionRecorderWidget/icons.tsx +9 -0
  85. package/src/components/SessionRecorderWidget/styles.ts +48 -35
  86. package/src/components/index.ts +3 -1
  87. package/src/config/defaults.ts +1 -0
  88. package/src/config/masking.ts +1 -0
  89. package/src/native/ScreenMasking.ts +34 -0
  90. package/src/native/SessionRecorderNative.ts +34 -0
  91. package/src/recorder/screenRecorder.ts +31 -2
  92. package/src/services/screenMaskingService.ts +118 -0
  93. package/src/types/session-recorder.ts +7 -1
  94. package/src/utils/nativeModuleTest.ts +60 -0
@@ -0,0 +1,168 @@
1
+ # Troubleshooting Native Module Issues
2
+
3
+ If you're seeing the warning "Screen masking native module is not available - auto-linking may still be in progress", follow these steps:
4
+
5
+ ## Quick Fix
6
+
7
+ 1. **Clean and Rebuild**:
8
+
9
+ ```bash
10
+ # Clean React Native cache
11
+ npx react-native start --reset-cache
12
+
13
+ # For iOS
14
+ cd ios && pod install && cd ..
15
+ npx react-native run-ios
16
+
17
+ # For Android
18
+ npx react-native run-android
19
+ ```
20
+
21
+ 2. **Check Auto-Linking**:
22
+ ```bash
23
+ # Check if auto-linking detected the module
24
+ npx react-native config
25
+ ```
26
+
27
+ ## Detailed Troubleshooting
28
+
29
+ ### 1. Verify Package Installation
30
+
31
+ Make sure the package is properly installed:
32
+
33
+ ```bash
34
+ npm list @multiplayer-app/session-recorder-react-native
35
+ ```
36
+
37
+ ### 2. Check Native Module Registration
38
+
39
+ The native module should be named `SessionRecorderNative`. You can verify this by checking the logs for:
40
+
41
+ ```
42
+ Available native modules: [..., SessionRecorderNative, ...]
43
+ ```
44
+
45
+ ### 3. iOS Specific Issues
46
+
47
+ **Check Podfile**:
48
+
49
+ ```ruby
50
+ # In your ios/Podfile, you should see:
51
+ pod 'SessionRecorderNative', :path => '../node_modules/@multiplayer-app/session-recorder-react-native/ios'
52
+ ```
53
+
54
+ **Clean iOS Build**:
55
+
56
+ ```bash
57
+ cd ios
58
+ rm -rf build
59
+ rm -rf Pods
60
+ rm Podfile.lock
61
+ pod install
62
+ cd ..
63
+ npx react-native run-ios
64
+ ```
65
+
66
+ ### 4. Android Specific Issues
67
+
68
+ **Check MainApplication.java**:
69
+
70
+ ```java
71
+ // Should be automatically added by auto-linking:
72
+ import com.multiplayer.sessionrecorder.SessionRecorderPackage;
73
+
74
+ // In getPackages():
75
+ new SessionRecorderPackage()
76
+ ```
77
+
78
+ **Clean Android Build**:
79
+
80
+ ```bash
81
+ cd android
82
+ ./gradlew clean
83
+ cd ..
84
+ npx react-native run-android
85
+ ```
86
+
87
+ ### 5. Manual Linking (If Auto-Linking Fails)
88
+
89
+ If auto-linking doesn't work, you can manually link:
90
+
91
+ **iOS**:
92
+
93
+ 1. Add to `ios/Podfile`:
94
+ ```ruby
95
+ pod 'SessionRecorderNative', :path => '../node_modules/@multiplayer-app/session-recorder-react-native/ios'
96
+ ```
97
+ 2. Run `pod install`
98
+
99
+ **Android**:
100
+
101
+ 1. Add to `android/settings.gradle`:
102
+ ```gradle
103
+ include ':react-native-session-recorder'
104
+ project(':react-native-session-recorder').projectDir = new File(rootProject.projectDir, '../node_modules/@multiplayer-app/session-recorder-react-native/android')
105
+ ```
106
+ 2. Add to `android/app/build.gradle`:
107
+ ```gradle
108
+ dependencies {
109
+ implementation project(':react-native-session-recorder')
110
+ }
111
+ ```
112
+ 3. Add to `MainApplication.java`:
113
+
114
+ ```java
115
+ import com.multiplayer.sessionrecorder.SessionRecorderPackage;
116
+
117
+ @Override
118
+ protected List<ReactPackage> getPackages() {
119
+ return Arrays.<ReactPackage>asList(
120
+ new MainReactPackage(),
121
+ new SessionRecorderPackage()
122
+ );
123
+ }
124
+ ```
125
+
126
+ ### 6. Debug Native Module Availability
127
+
128
+ Add this to your app to debug:
129
+
130
+ ```typescript
131
+ import { testNativeModuleAvailability } from '@multiplayer-app/session-recorder-react-native/utils'
132
+
133
+ // Call this in your app
134
+ testNativeModuleAvailability()
135
+ ```
136
+
137
+ This will log detailed information about native module availability.
138
+
139
+ ### 7. Common Issues
140
+
141
+ **Issue**: "Module not found" error
142
+ **Solution**: Ensure the package is installed and auto-linking is working
143
+
144
+ **Issue**: "Method not available" error
145
+ **Solution**: The native module is linked but methods aren't exposed. Check the native code.
146
+
147
+ **Issue**: Auto-linking not working
148
+ **Solution**: Check `react-native.config.js` and ensure React Native version is 0.60+
149
+
150
+ **Issue**: Pod install fails
151
+ **Solution**: Update CocoaPods: `sudo gem install cocoapods`
152
+
153
+ ### 8. Verification
154
+
155
+ Once fixed, you should see:
156
+
157
+ ```
158
+ ✅ SessionRecorderNative module is available!
159
+ ✅ captureAndMask method is available!
160
+ ✅ captureAndMaskWithOptions method is available!
161
+ ```
162
+
163
+ ## Still Having Issues?
164
+
165
+ 1. Check the [React Native Auto-Linking documentation](https://github.com/react-native-community/cli/blob/master/docs/autolinking.md)
166
+ 2. Verify your React Native version (0.60+ required)
167
+ 3. Check for conflicting native modules
168
+ 4. Try creating a fresh React Native project and testing the module there
@@ -0,0 +1,12 @@
1
+ #import <React/RCTBridgeModule.h>
2
+
3
+ @interface RCT_EXTERN_MODULE(SessionRecorder, NSObject)
4
+
5
+ RCT_EXTERN_METHOD(captureAndMask:(RCTPromiseResolveBlock)resolve
6
+ reject:(RCTPromiseRejectBlock)reject)
7
+
8
+ RCT_EXTERN_METHOD(captureAndMaskWithOptions:(NSDictionary *)options
9
+ resolve:(RCTPromiseResolveBlock)resolve
10
+ reject:(RCTPromiseRejectBlock)reject)
11
+
12
+ @end
@@ -0,0 +1,21 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "..", "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "SessionRecorder"
7
+ s.version = package["version"]
8
+ s.summary = "Native session recorder module for React Native"
9
+ s.description = "A native module that provides session recording with automatic masking of sensitive UI elements"
10
+ s.homepage = "https://github.com/multiplayer-app/multiplayer-session-recorder-javascript"
11
+ s.license = "MIT"
12
+ s.authors = { "Multiplayer Software, Inc." => "https://www.multiplayer.app" }
13
+ s.platforms = { :ios => "12.0" }
14
+ s.source = { :git => "https://github.com/multiplayer-app/multiplayer-session-recorder-javascript.git", :tag => "#{s.version}" }
15
+
16
+ s.source_files = "ios/**/*.{h,m,mm,swift}"
17
+ s.requires_arc = true
18
+
19
+ s.dependency "React-Core"
20
+ s.dependency "React"
21
+ end
@@ -0,0 +1,205 @@
1
+ import UIKit
2
+ import React
3
+
4
+ @objc(SessionRecorder)
5
+ class SessionRecorder: NSObject {
6
+
7
+ @objc func captureAndMask(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
8
+ DispatchQueue.main.async {
9
+ guard let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) else {
10
+ reject("NO_WINDOW", "Unable to get key window", nil)
11
+ return
12
+ }
13
+
14
+ UIGraphicsBeginImageContextWithOptions(window.bounds.size, false, UIScreen.main.scale)
15
+ window.drawHierarchy(in: window.bounds, afterScreenUpdates: true)
16
+ let screenshot = UIGraphicsGetImageFromCurrentImageContext()
17
+ UIGraphicsEndImageContext()
18
+
19
+ guard let image = screenshot else {
20
+ reject("CAPTURE_FAILED", "Failed to capture screen", nil)
21
+ return
22
+ }
23
+
24
+ // Apply masking to sensitive elements
25
+ let maskedImage = self.applyMasking(to: image, in: window)
26
+
27
+ if let data = maskedImage.jpegData(compressionQuality: 0.5) {
28
+ let base64 = data.base64EncodedString()
29
+ resolve(base64)
30
+ } else {
31
+ reject("ENCODING_FAILED", "Failed to encode image", nil)
32
+ }
33
+ }
34
+ }
35
+
36
+ @objc func captureAndMaskWithOptions(_ options: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
37
+ DispatchQueue.main.async {
38
+ guard let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) else {
39
+ reject("NO_WINDOW", "Unable to get key window", nil)
40
+ return
41
+ }
42
+
43
+ UIGraphicsBeginImageContextWithOptions(window.bounds.size, false, UIScreen.main.scale)
44
+ window.drawHierarchy(in: window.bounds, afterScreenUpdates: true)
45
+ let screenshot = UIGraphicsGetImageFromCurrentImageContext()
46
+ UIGraphicsEndImageContext()
47
+
48
+ guard let image = screenshot else {
49
+ reject("CAPTURE_FAILED", "Failed to capture screen", nil)
50
+ return
51
+ }
52
+
53
+ // Apply masking with custom options
54
+ let maskedImage = self.applyMaskingWithOptions(to: image, in: window, options: options)
55
+
56
+ if let data = maskedImage.jpegData(compressionQuality: 0.5) {
57
+ let base64 = data.base64EncodedString()
58
+ resolve(base64)
59
+ } else {
60
+ reject("ENCODING_FAILED", "Failed to encode image", nil)
61
+ }
62
+ }
63
+ }
64
+
65
+ private func applyMasking(to image: UIImage, in window: UIWindow) -> UIImage {
66
+ return applyMaskingWithOptions(to: image, in: window, options: [:])
67
+ }
68
+
69
+ private func applyMaskingWithOptions(to image: UIImage, in window: UIWindow, options: NSDictionary) -> UIImage {
70
+ UIGraphicsBeginImageContextWithOptions(image.size, false, image.scale)
71
+ guard let context = UIGraphicsGetCurrentContext() else { return image }
72
+
73
+ // Draw the original image
74
+ image.draw(in: CGRect(origin: .zero, size: image.size))
75
+
76
+ // Find and mask sensitive elements
77
+ let sensitiveElements = findSensitiveElements(in: window)
78
+
79
+ for element in sensitiveElements {
80
+ let frame = element.frame
81
+ let maskingType = getMaskingType(for: element)
82
+
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
92
+ }
93
+ }
94
+
95
+ let maskedImage = UIGraphicsGetImageFromCurrentImageContext() ?? image
96
+ UIGraphicsEndImageContext()
97
+
98
+ return maskedImage
99
+ }
100
+
101
+ private func findSensitiveElements(in view: UIView) -> [UIView] {
102
+ var sensitiveElements: [UIView] = []
103
+
104
+ func traverseView(_ view: UIView) {
105
+ // Check if this view should be masked
106
+ if shouldMaskView(view) {
107
+ sensitiveElements.append(view)
108
+ }
109
+
110
+ // Recursively check subviews
111
+ for subview in view.subviews {
112
+ traverseView(subview)
113
+ }
114
+ }
115
+
116
+ traverseView(view)
117
+ return sensitiveElements
118
+ }
119
+
120
+ private func shouldMaskView(_ view: UIView) -> Bool {
121
+ // Check for UITextField - mask all text fields when inputMasking is enabled
122
+ if view is UITextField {
123
+ return true
124
+ }
125
+
126
+ // Check for UITextView - mask all text views when inputMasking is enabled
127
+ if view is UITextView {
128
+ return true
129
+ }
130
+
131
+ return false
132
+ }
133
+
134
+ private func getMaskingType(for view: UIView) -> MaskingType {
135
+ // Default masking type for all text inputs
136
+ return .rectangle
137
+ }
138
+
139
+ private func applyBlurMask(in context: CGContext, frame: CGRect) {
140
+ // Create a blur effect
141
+ context.setFillColor(UIColor.black.withAlphaComponent(0.8).cgColor)
142
+ context.fill(frame)
143
+
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
+ }
152
+ }
153
+
154
+ private func applyRectangleMask(in context: CGContext, frame: CGRect) {
155
+ // Simple rectangle fill
156
+ context.setFillColor(UIColor.gray.cgColor)
157
+ context.fill(frame)
158
+
159
+ // Add some text-like pattern
160
+ context.setFillColor(UIColor.darkGray.cgColor)
161
+ let lineHeight: CGFloat = 4
162
+ let spacing: CGFloat = 8
163
+
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))
168
+ }
169
+ }
170
+
171
+ 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
+ }
196
+ }
197
+ }
198
+ }
199
+
200
+ private enum MaskingType {
201
+ case blur
202
+ case rectangle
203
+ case pixelate
204
+ case none
205
+ }
@@ -0,0 +1,12 @@
1
+ #import <React/RCTBridgeModule.h>
2
+
3
+ @interface RCT_EXTERN_MODULE(SessionRecorderNative, NSObject)
4
+
5
+ RCT_EXTERN_METHOD(captureAndMask:(RCTPromiseResolveBlock)resolve
6
+ reject:(RCTPromiseRejectBlock)reject)
7
+
8
+ RCT_EXTERN_METHOD(captureAndMaskWithOptions:(NSDictionary *)options
9
+ resolve:(RCTPromiseResolveBlock)resolve
10
+ reject:(RCTPromiseRejectBlock)reject)
11
+
12
+ @end
@@ -0,0 +1,21 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "..", "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "SessionRecorderNative"
7
+ s.version = package["version"]
8
+ s.summary = "Native session recorder module for React Native"
9
+ s.description = "A native module that provides session recording with automatic masking of sensitive UI elements"
10
+ s.homepage = "https://github.com/multiplayer-app/multiplayer-session-recorder-javascript"
11
+ s.license = "MIT"
12
+ s.authors = { "Multiplayer Software, Inc." => "https://www.multiplayer.app" }
13
+ s.platforms = { :ios => "12.0" }
14
+ s.source = { :git => "https://github.com/multiplayer-app/multiplayer-session-recorder-javascript.git", :tag => "#{s.version}" }
15
+
16
+ s.source_files = "ios/**/*.{h,m,mm,swift}"
17
+ s.requires_arc = true
18
+
19
+ s.dependency "React-Core"
20
+ s.dependency "React"
21
+ end
@@ -0,0 +1,205 @@
1
+ import UIKit
2
+ import React
3
+
4
+ @objc(SessionRecorderNative)
5
+ class SessionRecorderNative: NSObject {
6
+
7
+ @objc func captureAndMask(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
8
+ DispatchQueue.main.async {
9
+ guard let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) else {
10
+ reject("NO_WINDOW", "Unable to get key window", nil)
11
+ return
12
+ }
13
+
14
+ UIGraphicsBeginImageContextWithOptions(window.bounds.size, false, UIScreen.main.scale)
15
+ window.drawHierarchy(in: window.bounds, afterScreenUpdates: true)
16
+ let screenshot = UIGraphicsGetImageFromCurrentImageContext()
17
+ UIGraphicsEndImageContext()
18
+
19
+ guard let image = screenshot else {
20
+ reject("CAPTURE_FAILED", "Failed to capture screen", nil)
21
+ return
22
+ }
23
+
24
+ // Apply masking to sensitive elements
25
+ let maskedImage = self.applyMasking(to: image, in: window)
26
+
27
+ if let data = maskedImage.jpegData(compressionQuality: 0.5) {
28
+ let base64 = data.base64EncodedString()
29
+ resolve(base64)
30
+ } else {
31
+ reject("ENCODING_FAILED", "Failed to encode image", nil)
32
+ }
33
+ }
34
+ }
35
+
36
+ @objc func captureAndMaskWithOptions(_ options: NSDictionary, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) {
37
+ DispatchQueue.main.async {
38
+ guard let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) else {
39
+ reject("NO_WINDOW", "Unable to get key window", nil)
40
+ return
41
+ }
42
+
43
+ UIGraphicsBeginImageContextWithOptions(window.bounds.size, false, UIScreen.main.scale)
44
+ window.drawHierarchy(in: window.bounds, afterScreenUpdates: true)
45
+ let screenshot = UIGraphicsGetImageFromCurrentImageContext()
46
+ UIGraphicsEndImageContext()
47
+
48
+ guard let image = screenshot else {
49
+ reject("CAPTURE_FAILED", "Failed to capture screen", nil)
50
+ return
51
+ }
52
+
53
+ // Apply masking with custom options
54
+ let maskedImage = self.applyMaskingWithOptions(to: image, in: window, options: options)
55
+
56
+ if let data = maskedImage.jpegData(compressionQuality: 0.5) {
57
+ let base64 = data.base64EncodedString()
58
+ resolve(base64)
59
+ } else {
60
+ reject("ENCODING_FAILED", "Failed to encode image", nil)
61
+ }
62
+ }
63
+ }
64
+
65
+ private func applyMasking(to image: UIImage, in window: UIWindow) -> UIImage {
66
+ return applyMaskingWithOptions(to: image, in: window, options: [:])
67
+ }
68
+
69
+ private func applyMaskingWithOptions(to image: UIImage, in window: UIWindow, options: NSDictionary) -> UIImage {
70
+ UIGraphicsBeginImageContextWithOptions(image.size, false, image.scale)
71
+ guard let context = UIGraphicsGetCurrentContext() else { return image }
72
+
73
+ // Draw the original image
74
+ image.draw(in: CGRect(origin: .zero, size: image.size))
75
+
76
+ // Find and mask sensitive elements
77
+ let sensitiveElements = findSensitiveElements(in: window)
78
+
79
+ for element in sensitiveElements {
80
+ let frame = element.frame
81
+ let maskingType = getMaskingType(for: element)
82
+
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
92
+ }
93
+ }
94
+
95
+ let maskedImage = UIGraphicsGetImageFromCurrentImageContext() ?? image
96
+ UIGraphicsEndImageContext()
97
+
98
+ return maskedImage
99
+ }
100
+
101
+ private func findSensitiveElements(in view: UIView) -> [UIView] {
102
+ var sensitiveElements: [UIView] = []
103
+
104
+ func traverseView(_ view: UIView) {
105
+ // Check if this view should be masked
106
+ if shouldMaskView(view) {
107
+ sensitiveElements.append(view)
108
+ }
109
+
110
+ // Recursively check subviews
111
+ for subview in view.subviews {
112
+ traverseView(subview)
113
+ }
114
+ }
115
+
116
+ traverseView(view)
117
+ return sensitiveElements
118
+ }
119
+
120
+ private func shouldMaskView(_ view: UIView) -> Bool {
121
+ // Check for UITextField - mask all text fields when inputMasking is enabled
122
+ if view is UITextField {
123
+ return true
124
+ }
125
+
126
+ // Check for UITextView - mask all text views when inputMasking is enabled
127
+ if view is UITextView {
128
+ return true
129
+ }
130
+
131
+ return false
132
+ }
133
+
134
+ private func getMaskingType(for view: UIView) -> MaskingType {
135
+ // Default masking type for all text inputs
136
+ return .rectangle
137
+ }
138
+
139
+ private func applyBlurMask(in context: CGContext, frame: CGRect) {
140
+ // Create a blur effect
141
+ context.setFillColor(UIColor.black.withAlphaComponent(0.8).cgColor)
142
+ context.fill(frame)
143
+
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
+ }
152
+ }
153
+
154
+ private func applyRectangleMask(in context: CGContext, frame: CGRect) {
155
+ // Simple rectangle fill
156
+ context.setFillColor(UIColor.gray.cgColor)
157
+ context.fill(frame)
158
+
159
+ // Add some text-like pattern
160
+ context.setFillColor(UIColor.darkGray.cgColor)
161
+ let lineHeight: CGFloat = 4
162
+ let spacing: CGFloat = 8
163
+
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))
168
+ }
169
+ }
170
+
171
+ 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
+ }
196
+ }
197
+ }
198
+ }
199
+
200
+ private enum MaskingType {
201
+ case blur
202
+ case rectangle
203
+ case pixelate
204
+ case none
205
+ }
@@ -0,0 +1,21 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "..", "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "SessionRecorderNative"
7
+ s.version = package["version"]
8
+ s.summary = "Native session recorder module for React Native"
9
+ s.description = "A native module that provides session recording with automatic masking of sensitive UI elements"
10
+ s.homepage = "https://github.com/multiplayer-app/multiplayer-session-recorder-javascript"
11
+ s.license = "MIT"
12
+ s.authors = { "Multiplayer Software, Inc." => "https://www.multiplayer.app" }
13
+ s.platforms = { :ios => "12.0" }
14
+ s.source = { :git => "https://github.com/multiplayer-app/multiplayer-session-recorder-javascript.git", :tag => "#{s.version}" }
15
+
16
+ s.source_files = "ios/**/*.{h,m,mm,swift}"
17
+ s.requires_arc = true
18
+
19
+ s.dependency "React-Core"
20
+ s.dependency "React"
21
+ end
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.4",
3
+ "version": "0.0.1-beta.6",
4
4
  "description": "Multiplayer Fullstack Session Recorder for React Native",
5
5
  "author": {
6
6
  "name": "Multiplayer Software, Inc.",
@@ -88,5 +88,14 @@
88
88
  },
89
89
  "optionalDependencies": {
90
90
  "expo-constants": "^15.0.0"
91
+ },
92
+ "react-native": {
93
+ "ios": {
94
+ "podspecPath": "./ios/SessionRecorderNative.podspec"
95
+ },
96
+ "android": {
97
+ "sourceDir": "./android",
98
+ "packageImportPath": "import com.multiplayer.sessionrecorder.SessionRecorderPackage;"
99
+ }
91
100
  }
92
101
  }