@multiplayer-app/session-recorder-react-native 0.0.1 → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +708 -83
- package/SessionRecorderNative.podspec +26 -0
- package/android/build.gradle +34 -0
- package/copy-react-native-dist.sh +34 -16
- package/dist/components/ScreenRecorderView/ScreenRecorderView.js +1 -1
- package/dist/components/ScreenRecorderView/ScreenRecorderView.js.map +1 -1
- package/dist/components/SessionRecorderWidget/ErrorBanner.d.ts +7 -0
- package/dist/components/SessionRecorderWidget/ErrorBanner.js +1 -0
- package/dist/components/SessionRecorderWidget/ErrorBanner.js.map +1 -0
- package/dist/components/SessionRecorderWidget/FinalPopover.d.ts +12 -0
- package/dist/components/SessionRecorderWidget/FinalPopover.js +1 -0
- package/dist/components/SessionRecorderWidget/FinalPopover.js.map +1 -0
- package/dist/components/SessionRecorderWidget/FloatingButton.d.ts +8 -0
- package/dist/components/SessionRecorderWidget/FloatingButton.js +1 -0
- package/dist/components/SessionRecorderWidget/FloatingButton.js.map +1 -0
- package/dist/components/SessionRecorderWidget/InitialPopover.d.ts +16 -0
- package/dist/components/SessionRecorderWidget/InitialPopover.js +1 -0
- package/dist/components/SessionRecorderWidget/InitialPopover.js.map +1 -0
- package/dist/components/SessionRecorderWidget/ModalContainer.d.ts +8 -0
- package/dist/components/SessionRecorderWidget/ModalContainer.js +1 -0
- package/dist/components/SessionRecorderWidget/ModalContainer.js.map +1 -0
- package/dist/components/SessionRecorderWidget/ModalHeader.d.ts +6 -0
- package/dist/components/SessionRecorderWidget/ModalHeader.js +1 -0
- package/dist/components/SessionRecorderWidget/ModalHeader.js.map +1 -0
- package/dist/components/SessionRecorderWidget/SessionRecorderWidget.d.ts +5 -0
- package/dist/components/SessionRecorderWidget/SessionRecorderWidget.js +1 -0
- package/dist/components/SessionRecorderWidget/SessionRecorderWidget.js.map +1 -0
- package/dist/components/SessionRecorderWidget/icons.d.ts +11 -0
- package/dist/components/SessionRecorderWidget/icons.js +1 -0
- package/dist/components/SessionRecorderWidget/icons.js.map +1 -0
- package/dist/components/SessionRecorderWidget/index.d.ts +2 -0
- package/dist/components/SessionRecorderWidget/index.js +1 -0
- package/dist/components/SessionRecorderWidget/index.js.map +1 -0
- package/dist/components/SessionRecorderWidget/styles.d.ts +165 -0
- package/dist/components/SessionRecorderWidget/styles.js +1 -0
- package/dist/components/SessionRecorderWidget/styles.js.map +1 -0
- package/dist/components/index.d.ts +2 -1
- package/dist/components/index.js +1 -1
- package/dist/components/index.js.map +1 -1
- package/dist/config/constants.js +1 -1
- package/dist/config/constants.js.map +1 -1
- package/dist/config/defaults.d.ts +4 -4
- package/dist/config/defaults.js +1 -1
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/masking.d.ts +2 -2
- package/dist/config/masking.js +1 -1
- package/dist/config/masking.js.map +1 -1
- package/dist/config/session-recorder.js +1 -1
- package/dist/config/session-recorder.js.map +1 -1
- package/dist/config/validators.d.ts +1 -1
- package/dist/config/validators.js +1 -1
- package/dist/config/validators.js.map +1 -1
- package/dist/config/widget.d.ts +9 -0
- package/dist/config/widget.js +1 -0
- package/dist/config/widget.js.map +1 -0
- package/dist/context/SessionRecorderContext.d.ts +12 -3
- package/dist/context/SessionRecorderContext.js +1 -1
- package/dist/context/SessionRecorderContext.js.map +1 -1
- package/dist/context/SessionRecorderStore.d.ts +12 -0
- package/dist/context/SessionRecorderStore.js +1 -0
- package/dist/context/SessionRecorderStore.js.map +1 -0
- package/dist/context/useSessionRecorderStore.d.ts +8 -0
- package/dist/context/useSessionRecorderStore.js +1 -0
- package/dist/context/useSessionRecorderStore.js.map +1 -0
- package/dist/context/useStoreSelector.d.ts +4 -0
- package/dist/context/useStoreSelector.js +1 -0
- package/dist/context/useStoreSelector.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/native/GestureRecorderNative.d.ts +57 -0
- package/dist/native/GestureRecorderNative.js +1 -0
- package/dist/native/GestureRecorderNative.js.map +1 -0
- package/dist/native/SessionRecorderNative.d.ts +33 -0
- package/dist/native/SessionRecorderNative.js +1 -0
- package/dist/native/SessionRecorderNative.js.map +1 -0
- package/dist/native/index.d.ts +2 -0
- package/dist/native/index.js +1 -0
- package/dist/native/index.js.map +1 -0
- package/dist/otel/index.js +1 -1
- package/dist/otel/index.js.map +1 -1
- package/dist/patch/xhr.js +1 -1
- package/dist/patch/xhr.js.map +1 -1
- package/dist/recorder/eventExporter.d.ts +4 -1
- package/dist/recorder/eventExporter.js +1 -1
- package/dist/recorder/eventExporter.js.map +1 -1
- package/dist/recorder/gestureRecorder.d.ts +28 -62
- package/dist/recorder/gestureRecorder.js +1 -1
- package/dist/recorder/gestureRecorder.js.map +1 -1
- package/dist/recorder/index.d.ts +2 -0
- package/dist/recorder/index.js +1 -1
- package/dist/recorder/index.js.map +1 -1
- package/dist/recorder/navigationTracker.d.ts +4 -19
- package/dist/recorder/navigationTracker.js +1 -1
- package/dist/recorder/navigationTracker.js.map +1 -1
- package/dist/recorder/screenRecorder.d.ts +11 -5
- package/dist/recorder/screenRecorder.js +1 -1
- package/dist/recorder/screenRecorder.js.map +1 -1
- package/dist/services/api.service.d.ts +12 -3
- package/dist/services/api.service.js +1 -1
- package/dist/services/api.service.js.map +1 -1
- package/dist/services/network.service.d.ts +46 -0
- package/dist/services/network.service.js +1 -0
- package/dist/services/network.service.js.map +1 -0
- package/dist/services/screenMaskingService.d.ts +47 -0
- package/dist/services/screenMaskingService.js +1 -0
- package/dist/services/screenMaskingService.js.map +1 -0
- package/dist/services/storage.service.d.ts +18 -2
- package/dist/services/storage.service.js +1 -1
- package/dist/services/storage.service.js.map +1 -1
- package/dist/session-recorder.d.ts +18 -33
- package/dist/session-recorder.js +1 -1
- package/dist/session-recorder.js.map +1 -1
- package/dist/types/configs.d.ts +85 -0
- package/dist/types/configs.js +1 -0
- package/dist/types/configs.js.map +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/session-recorder.d.ts +105 -132
- package/dist/types/session-recorder.js +1 -1
- package/dist/types/session-recorder.js.map +1 -1
- package/dist/utils/constants.optional.d.ts +21 -0
- package/dist/utils/constants.optional.expo.d.ts +3 -0
- package/dist/utils/constants.optional.expo.js +1 -0
- package/dist/utils/constants.optional.expo.js.map +1 -0
- package/dist/utils/constants.optional.js +1 -0
- package/dist/utils/constants.optional.js.map +1 -0
- package/dist/utils/createStore.d.ts +8 -0
- package/dist/utils/createStore.js +1 -0
- package/dist/utils/createStore.js.map +1 -0
- package/dist/utils/logger.d.ts +2 -7
- package/dist/utils/logger.js +1 -1
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/platform.d.ts +11 -0
- package/dist/utils/platform.js +1 -1
- package/dist/utils/platform.js.map +1 -1
- package/dist/utils/rrweb-events.d.ts +4 -3
- package/dist/utils/rrweb-events.js +1 -1
- package/dist/utils/rrweb-events.js.map +1 -1
- package/dist/utils/session.d.ts +2 -1
- package/dist/utils/session.js +1 -1
- package/dist/utils/session.js.map +1 -1
- package/dist/utils/shallowEqual.d.ts +1 -0
- package/dist/utils/shallowEqual.js +1 -0
- package/dist/utils/shallowEqual.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/ios/GestureRecorderNative.m +21 -0
- package/ios/GestureRecorderNative.swift +316 -0
- package/ios/SessionRecorderNative.m +17 -0
- package/ios/SessionRecorderNative.podspec +26 -0
- package/ios/SessionRecorderNative.swift +599 -0
- package/package.json +15 -16
- package/react-native.config.js +12 -0
- package/RRWEB_INTEGRATION.md +0 -336
- package/VIEWSHOT_INTEGRATION_TEST.md +0 -123
- package/babel.config.js +0 -13
- package/dist/components/GestureCaptureWrapper/GestureCaptureWrapper.d.ts +0 -6
- package/dist/components/GestureCaptureWrapper/GestureCaptureWrapper.js +0 -1
- package/dist/components/GestureCaptureWrapper/GestureCaptureWrapper.js.map +0 -1
- package/dist/components/GestureCaptureWrapper/index.d.ts +0 -1
- package/dist/components/GestureCaptureWrapper/index.js +0 -1
- package/dist/components/GestureCaptureWrapper/index.js.map +0 -1
- package/dist/components/GestureCaptureWrapper.d.ts +0 -6
- package/dist/components/GestureCaptureWrapper.js +0 -1
- package/dist/components/GestureCaptureWrapper.js.map +0 -1
- package/dist/expo.d.ts +0 -7
- package/dist/expo.js +0 -1
- package/dist/expo.js.map +0 -1
- package/dist/otel/instrumentations/gestureInstrumentation.d.ts +0 -15
- package/dist/otel/instrumentations/gestureInstrumentation.js +0 -1
- package/dist/otel/instrumentations/gestureInstrumentation.js.map +0 -1
- package/dist/otel/instrumentations/reactNativeInstrumentation.d.ts +0 -8
- package/dist/otel/instrumentations/reactNativeInstrumentation.js +0 -1
- package/dist/otel/instrumentations/reactNativeInstrumentation.js.map +0 -1
- package/dist/otel/instrumentations/reactNavigationInstrumentation.d.ts +0 -13
- package/dist/otel/instrumentations/reactNavigationInstrumentation.js +0 -1
- package/dist/otel/instrumentations/reactNavigationInstrumentation.js.map +0 -1
- package/dist/recorder/gestureHandlerRecorder.d.ts +0 -19
- package/dist/recorder/gestureHandlerRecorder.js +0 -1
- package/dist/recorder/gestureHandlerRecorder.js.map +0 -1
- package/dist/types/rrweb.d.ts +0 -118
- package/dist/types/rrweb.js +0 -1
- package/dist/types/rrweb.js.map +0 -1
- package/scripts/generate-app-metadata.js +0 -173
- package/src/components/GestureCaptureWrapper/GestureCaptureWrapper.tsx +0 -86
- package/src/components/GestureCaptureWrapper/index.ts +0 -1
- package/src/components/ScreenRecorderView/ScreenRecorderView.tsx +0 -72
- package/src/components/ScreenRecorderView/index.ts +0 -1
- package/src/components/index.ts +0 -1
- package/src/config/constants.ts +0 -60
- package/src/config/defaults.ts +0 -82
- package/src/config/index.ts +0 -6
- package/src/config/masking.ts +0 -27
- package/src/config/session-recorder.ts +0 -55
- package/src/config/validators.ts +0 -31
- package/src/context/SessionRecorderContext.tsx +0 -75
- package/src/expo.ts +0 -11
- package/src/index.ts +0 -17
- package/src/otel/helpers.ts +0 -275
- package/src/otel/index.ts +0 -138
- package/src/otel/instrumentations/index.ts +0 -115
- package/src/patch/index.ts +0 -1
- package/src/patch/xhr.ts +0 -142
- package/src/recorder/eventExporter.ts +0 -141
- package/src/recorder/gestureRecorder.ts +0 -498
- package/src/recorder/index.ts +0 -179
- package/src/recorder/navigationTracker.ts +0 -449
- package/src/recorder/screenRecorder.ts +0 -498
- package/src/services/api.service.ts +0 -203
- package/src/services/storage.service.ts +0 -158
- package/src/session-recorder.ts +0 -600
- package/src/types/expo.d.ts +0 -23
- package/src/types/index.ts +0 -28
- package/src/types/session-recorder.ts +0 -423
- package/src/types/session.ts +0 -65
- package/src/utils/app-metadata.ts +0 -31
- package/src/utils/index.ts +0 -8
- package/src/utils/logger.ts +0 -225
- package/src/utils/platform.ts +0 -384
- package/src/utils/request-utils.ts +0 -61
- package/src/utils/rrweb-events.ts +0 -309
- package/src/utils/session.ts +0 -18
- package/src/utils/time.ts +0 -17
- package/src/utils/type-utils.ts +0 -75
- package/src/version.ts +0 -1
- 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-beta.1",
|
|
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
|
-
"
|
|
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-
|
|
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": "
|
|
85
|
+
"expo-constants": "*",
|
|
91
86
|
"react": ">=18.0.0 <20.0.0",
|
|
92
|
-
"react-native": ">=0.72.0 <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
|
-
"
|
|
95
|
-
"expo-constants":
|
|
91
|
+
"peerDependenciesMeta": {
|
|
92
|
+
"expo-constants": {
|
|
93
|
+
"optional": true
|
|
94
|
+
}
|
|
96
95
|
}
|
|
97
|
-
}
|
|
96
|
+
}
|