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

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 (88) 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/reactNativeHierarchyExtractor.d.ts +38 -0
  58. package/dist/utils/reactNativeHierarchyExtractor.js +1 -0
  59. package/dist/utils/reactNativeHierarchyExtractor.js.map +1 -0
  60. package/dist/utils/screenshotMasker.d.ts +96 -0
  61. package/dist/utils/screenshotMasker.js +1 -0
  62. package/dist/utils/screenshotMasker.js.map +1 -0
  63. package/dist/utils/viewHierarchyTracker.d.ts +89 -0
  64. package/dist/utils/viewHierarchyTracker.js +1 -0
  65. package/dist/utils/viewHierarchyTracker.js.map +1 -0
  66. package/ios/ScreenMasking.m +12 -0
  67. package/ios/ScreenMasking.podspec +21 -0
  68. package/ios/ScreenMasking.swift +205 -0
  69. package/ios/SessionRecorder.m +12 -0
  70. package/ios/SessionRecorder.podspec +21 -0
  71. package/ios/SessionRecorder.swift +205 -0
  72. package/package.json +10 -1
  73. package/react-native.config.js +15 -0
  74. package/src/components/SessionRecorderWidget/FinalPopover.tsx +5 -16
  75. package/src/components/SessionRecorderWidget/FloatingButton.tsx +14 -27
  76. package/src/components/SessionRecorderWidget/InitialPopover.tsx +3 -9
  77. package/src/components/SessionRecorderWidget/ModalContainer.tsx +77 -29
  78. package/src/components/SessionRecorderWidget/ModalHeader.tsx +24 -0
  79. package/src/components/SessionRecorderWidget/icons.tsx +9 -0
  80. package/src/components/SessionRecorderWidget/styles.ts +48 -35
  81. package/src/components/index.ts +3 -1
  82. package/src/config/defaults.ts +1 -0
  83. package/src/config/masking.ts +1 -0
  84. package/src/native/ScreenMasking.ts +34 -0
  85. package/src/native/SessionRecorderNative.ts +34 -0
  86. package/src/recorder/screenRecorder.ts +31 -2
  87. package/src/services/screenMaskingService.ts +114 -0
  88. package/src/types/session-recorder.ts +7 -1
@@ -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(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
+ }
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.5",
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/SessionRecorder.podspec"
95
+ },
96
+ "android": {
97
+ "sourceDir": "./android",
98
+ "packageImportPath": "import com.multiplayer.sessionrecorder.SessionRecorderPackage;"
99
+ }
91
100
  }
92
101
  }
@@ -0,0 +1,15 @@
1
+ module.exports = {
2
+ dependencies: {
3
+ 'react-native-session-recorder': {
4
+ platforms: {
5
+ android: {
6
+ sourceDir: '../android',
7
+ packageImportPath: 'import com.multiplayer.sessionrecorder.SessionRecorderPackage;'
8
+ },
9
+ ios: {
10
+ podspecPath: '../ios/SessionRecorder.podspec'
11
+ }
12
+ }
13
+ }
14
+ }
15
+ }
@@ -1,7 +1,8 @@
1
1
  import React, { useState } from 'react'
2
- import { View, Text, Pressable, TextInput, Linking, Alert } from 'react-native'
2
+ import { View, Text, Pressable, TextInput, Alert } from 'react-native'
3
3
  import { WidgetTextOverridesConfig } from '../../types'
4
4
  import { sharedStyles } from './styles'
5
+ import ModalHeader from './ModalHeader'
5
6
 
6
7
  interface FinalPopoverProps {
7
8
  textOverrides: WidgetTextOverridesConfig
@@ -11,13 +12,7 @@ interface FinalPopoverProps {
11
12
  isSubmitting: boolean
12
13
  }
13
14
 
14
- const FinalPopover: React.FC<FinalPopoverProps> = ({
15
- textOverrides,
16
- onStopRecording,
17
- onCancelSession,
18
- onClose,
19
- isSubmitting
20
- }) => {
15
+ const FinalPopover: React.FC<FinalPopoverProps> = ({ textOverrides, onStopRecording, onCancelSession, isSubmitting }) => {
21
16
  const [comment, setComment] = useState('')
22
17
 
23
18
  const handleStopRecording = async () => {
@@ -30,17 +25,11 @@ const FinalPopover: React.FC<FinalPopoverProps> = ({
30
25
 
31
26
  return (
32
27
  <View style={sharedStyles.popoverContent}>
33
- <View style={sharedStyles.popoverHeader}>
34
- <Pressable onPress={() => Linking.openURL('https://www.multiplayer.app')}>
35
- <Text style={sharedStyles.logoText}>Multiplayer</Text>
36
- </Pressable>
28
+ <ModalHeader>
37
29
  <Pressable onPress={onCancelSession} style={sharedStyles.cancelButton}>
38
30
  <Text style={sharedStyles.cancelButtonText}>{textOverrides.cancelButtonText}</Text>
39
31
  </Pressable>
40
- <Pressable onPress={onClose} style={sharedStyles.closeButton}>
41
- <Text style={sharedStyles.closeButtonText}>×</Text>
42
- </Pressable>
43
- </View>
32
+ </ModalHeader>
44
33
 
45
34
  <View style={sharedStyles.popoverBody}>
46
35
  <Text style={sharedStyles.title}>{textOverrides.finalTitle}</Text>
@@ -8,20 +8,21 @@ interface FloatingButtonProps {
8
8
  sessionState: SessionState | null
9
9
  onPress: () => void
10
10
  }
11
- const buttonSize = 60
11
+
12
+ const buttonSize = 52
12
13
  const rightOffset = 20
13
14
  const topOffset = Platform.OS === 'ios' ? 60 : 40
14
15
 
15
16
  const FloatingButton: React.FC<FloatingButtonProps> = ({ sessionState, onPress }) => {
16
- const position = useRef(new Animated.ValueXY({ x: 0, y: 0 })).current // For tracking position
17
- const lastPosition = useRef({ top: topOffset, right: rightOffset }) // Track the last saved position
18
- const storageService = useRef(StorageService.getInstance()).current // Singleton instance
17
+ const position = useRef(new Animated.ValueXY({ x: 0, y: 0 })).current
18
+ const lastPosition = useRef({ top: topOffset, right: rightOffset })
19
+ const storageService = useRef(StorageService.getInstance()).current
19
20
 
20
21
  const screenBounds = useMemo(() => {
21
22
  const { width, height } = Dimensions.get('window')
22
23
 
23
24
  return {
24
- minTop: topOffset, // Account for status bar
25
+ minTop: topOffset,
25
26
  maxTop: height - buttonSize,
26
27
  minRight: 0,
27
28
  maxRight: width - buttonSize
@@ -32,14 +33,12 @@ const FloatingButton: React.FC<FloatingButtonProps> = ({ sessionState, onPress }
32
33
  useEffect(() => {
33
34
  const savedPosition = storageService.getFloatingButtonPosition()
34
35
  if (savedPosition) {
35
- // Convert from x,y coordinates to top,right coordinates
36
- const { width, height } = Dimensions.get('window')
36
+ const { width } = Dimensions.get('window')
37
37
  const top = savedPosition.y
38
38
  const right = width - savedPosition.x - buttonSize
39
39
  lastPosition.current = { top, right }
40
40
  position.setValue({ x: right, y: top })
41
41
  } else {
42
- // Set default position
43
42
  position.setValue({ x: lastPosition.current.right, y: lastPosition.current.top })
44
43
  }
45
44
  }, [])
@@ -48,7 +47,6 @@ const FloatingButton: React.FC<FloatingButtonProps> = ({ sessionState, onPress }
48
47
  PanResponder.create({
49
48
  onStartShouldSetPanResponder: () => true,
50
49
  onMoveShouldSetPanResponder: (evt, gestureState) => {
51
- // Only start dragging if movement is significant enough
52
50
  const distance = Math.sqrt(gestureState.dx * gestureState.dx + gestureState.dy * gestureState.dy)
53
51
  return distance > 5
54
52
  },
@@ -59,7 +57,7 @@ const FloatingButton: React.FC<FloatingButtonProps> = ({ sessionState, onPress }
59
57
  onPanResponderMove: (evt, gestureState) => {
60
58
  // Calculate new position based on gesture movement
61
59
  const newTop = lastPosition.current.top + gestureState.dy
62
- const newRight = lastPosition.current.right - gestureState.dx // Invert dx for right positioning
60
+ const newRight = lastPosition.current.right - gestureState.dx
63
61
 
64
62
  // Update position during drag
65
63
  position.setValue({ x: newRight, y: newTop })
@@ -74,7 +72,7 @@ const FloatingButton: React.FC<FloatingButtonProps> = ({ sessionState, onPress }
74
72
  } else {
75
73
  // Calculate new position after dragging
76
74
  const newTop = lastPosition.current.top + gestureState.dy
77
- const newRight = lastPosition.current.right - gestureState.dx // Invert dx for right positioning
75
+ const newRight = lastPosition.current.right - gestureState.dx
78
76
 
79
77
  // Clamp to screen bounds
80
78
  const clampedTop = Math.max(screenBounds.minTop, Math.min(screenBounds.maxTop, newTop))
@@ -99,31 +97,20 @@ const FloatingButton: React.FC<FloatingButtonProps> = ({ sessionState, onPress }
99
97
  ).current
100
98
 
101
99
  // Memoized button icon and color for performance
102
- const buttonIcon = useMemo(() => {
103
- switch (sessionState) {
104
- case SessionState.started:
105
- return <CapturingIcon size={24} color='white' />
106
- case SessionState.paused:
107
- return <PausedIcon size={24} color='white' />
108
- default:
109
- return <RecordIcon size={19} color='white' />
110
- }
111
- }, [sessionState])
112
-
113
- const buttonColor = useMemo(() => {
100
+ const content = useMemo(() => {
114
101
  switch (sessionState) {
115
102
  case SessionState.started:
116
- return '#FF4444'
103
+ return { icon: <CapturingIcon size={28} color='white' />, color: '#FF4444' }
117
104
  case SessionState.paused:
118
- return '#FFA500'
105
+ return { icon: <PausedIcon size={28} color='white' />, color: '#FFA500' }
119
106
  default:
120
- return '#007AFF'
107
+ return { icon: <RecordIcon size={28} color='#718096' />, color: '#ffffff' }
121
108
  }
122
109
  }, [sessionState])
123
110
 
124
111
  return (
125
112
  <Animated.View style={[styles.draggableButton, { top: position.y, right: position.x }]} {...panResponder.panHandlers}>
126
- <View style={[styles.floatingButton, { backgroundColor: buttonColor }]}>{buttonIcon}</View>
113
+ <View style={[styles.floatingButton, { backgroundColor: content.color }]}>{content.icon}</View>
127
114
  </Animated.View>
128
115
  )
129
116
  }
@@ -1,8 +1,9 @@
1
1
  import React, { useState } from 'react'
2
- import { View, Text, Pressable, Switch, Linking, Alert } from 'react-native'
2
+ import { View, Text, Pressable, Alert, Switch } from 'react-native'
3
3
  import { SessionType } from '@multiplayer-app/session-recorder-common'
4
4
  import { WidgetTextOverridesConfig } from '../../types'
5
5
  import { sharedStyles } from './styles'
6
+ import ModalHeader from './ModalHeader'
6
7
 
7
8
  interface InitialPopoverProps {
8
9
  textOverrides: WidgetTextOverridesConfig
@@ -34,14 +35,7 @@ const InitialPopover: React.FC<InitialPopoverProps> = ({
34
35
 
35
36
  return (
36
37
  <View style={sharedStyles.popoverContent}>
37
- <View style={sharedStyles.popoverHeader}>
38
- <Pressable onPress={() => Linking.openURL('https://www.multiplayer.app')}>
39
- <Text style={sharedStyles.logoText}>Multiplayer</Text>
40
- </Pressable>
41
- <Pressable onPress={onClose} style={sharedStyles.closeButton}>
42
- <Text style={sharedStyles.closeButtonText}>×</Text>
43
- </Pressable>
44
- </View>
38
+ <ModalHeader />
45
39
 
46
40
  <View style={sharedStyles.popoverBody}>
47
41
  {showContinuousRecording && (