@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.
- package/android/build.gradle +32 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/multiplayer/sessionrecorder/ScreenMaskingModule.kt +202 -0
- package/android/src/main/java/com/multiplayer/sessionrecorder/ScreenMaskingPackage.kt +16 -0
- package/android/src/main/java/com/multiplayer/sessionrecorder/SessionRecorderModule.kt +202 -0
- package/android/src/main/java/com/multiplayer/sessionrecorder/SessionRecorderPackage.kt +16 -0
- package/dist/components/MaskableComponent.d.ts +22 -0
- package/dist/components/MaskableComponent.js +1 -0
- package/dist/components/MaskableComponent.js.map +1 -0
- package/dist/components/MaskableTextInput.d.ts +14 -0
- package/dist/components/MaskableTextInput.js +1 -0
- package/dist/components/MaskableTextInput.js.map +1 -0
- package/dist/components/SessionRecorderWidget/FinalPopover.js +1 -1
- package/dist/components/SessionRecorderWidget/FinalPopover.js.map +1 -1
- package/dist/components/SessionRecorderWidget/FloatingButton.js +1 -1
- package/dist/components/SessionRecorderWidget/FloatingButton.js.map +1 -1
- package/dist/components/SessionRecorderWidget/InitialPopover.js +1 -1
- package/dist/components/SessionRecorderWidget/InitialPopover.js.map +1 -1
- package/dist/components/SessionRecorderWidget/ModalContainer.js +1 -1
- package/dist/components/SessionRecorderWidget/ModalContainer.js.map +1 -1
- 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/icons.d.ts +1 -0
- package/dist/components/SessionRecorderWidget/icons.js +1 -1
- package/dist/components/SessionRecorderWidget/icons.js.map +1 -1
- package/dist/components/SessionRecorderWidget/styles.d.ts +39 -22
- package/dist/components/SessionRecorderWidget/styles.js +1 -1
- package/dist/components/SessionRecorderWidget/styles.js.map +1 -1
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.js +1 -1
- package/dist/components/index.js.map +1 -1
- package/dist/config/defaults.js +1 -1
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/masking.js +1 -1
- package/dist/config/masking.js.map +1 -1
- package/dist/native/ScreenMasking.d.ts +21 -0
- package/dist/native/ScreenMasking.js +1 -0
- package/dist/native/ScreenMasking.js.map +1 -0
- package/dist/native/SessionRecorderNative.d.ts +21 -0
- package/dist/native/SessionRecorderNative.js +1 -0
- package/dist/native/SessionRecorderNative.js.map +1 -0
- package/dist/recorder/screenRecorder.d.ts +1 -0
- package/dist/recorder/screenRecorder.js +1 -1
- package/dist/recorder/screenRecorder.js.map +1 -1
- package/dist/recorder/screenshotManager.d.ts +10 -0
- package/dist/recorder/screenshotManager.js +1 -0
- package/dist/recorder/screenshotManager.js.map +1 -0
- package/dist/services/screenMaskingService.d.ts +39 -0
- package/dist/services/screenMaskingService.js +1 -0
- package/dist/services/screenMaskingService.js.map +1 -0
- package/dist/types/session-recorder.d.ts +6 -0
- package/dist/types/session-recorder.js.map +1 -1
- package/dist/utils/componentRegistry.d.ts +64 -0
- package/dist/utils/componentRegistry.js +1 -0
- package/dist/utils/componentRegistry.js.map +1 -0
- package/dist/utils/reactNativeHierarchyExtractor.d.ts +38 -0
- package/dist/utils/reactNativeHierarchyExtractor.js +1 -0
- package/dist/utils/reactNativeHierarchyExtractor.js.map +1 -0
- package/dist/utils/screenshotMasker.d.ts +96 -0
- package/dist/utils/screenshotMasker.js +1 -0
- package/dist/utils/screenshotMasker.js.map +1 -0
- package/dist/utils/viewHierarchyTracker.d.ts +89 -0
- package/dist/utils/viewHierarchyTracker.js +1 -0
- package/dist/utils/viewHierarchyTracker.js.map +1 -0
- package/ios/ScreenMasking.m +12 -0
- package/ios/ScreenMasking.podspec +21 -0
- package/ios/ScreenMasking.swift +205 -0
- package/ios/SessionRecorder.m +12 -0
- package/ios/SessionRecorder.podspec +21 -0
- package/ios/SessionRecorder.swift +205 -0
- package/package.json +10 -1
- package/react-native.config.js +15 -0
- package/src/components/SessionRecorderWidget/FinalPopover.tsx +5 -16
- package/src/components/SessionRecorderWidget/FloatingButton.tsx +14 -27
- package/src/components/SessionRecorderWidget/InitialPopover.tsx +3 -9
- package/src/components/SessionRecorderWidget/ModalContainer.tsx +77 -29
- package/src/components/SessionRecorderWidget/ModalHeader.tsx +24 -0
- package/src/components/SessionRecorderWidget/icons.tsx +9 -0
- package/src/components/SessionRecorderWidget/styles.ts +48 -35
- package/src/components/index.ts +3 -1
- package/src/config/defaults.ts +1 -0
- package/src/config/masking.ts +1 -0
- package/src/native/ScreenMasking.ts +34 -0
- package/src/native/SessionRecorderNative.ts +34 -0
- package/src/recorder/screenRecorder.ts +31 -2
- package/src/services/screenMaskingService.ts +114 -0
- package/src/types/session-recorder.ts +7 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import React, { useEffect, useRef, useState } from 'react'
|
|
2
|
-
import { Animated,
|
|
2
|
+
import { Animated, Pressable, StyleSheet, Dimensions, Modal, SafeAreaView } from 'react-native'
|
|
3
|
+
import { Gesture, GestureDetector, GestureHandlerRootView } from 'react-native-gesture-handler'
|
|
3
4
|
|
|
4
5
|
const { height: SCREEN_HEIGHT } = Dimensions.get('window')
|
|
5
6
|
const MODAL_HEIGHT = SCREEN_HEIGHT * 0.7
|
|
@@ -12,28 +13,86 @@ interface ModalContainerProps {
|
|
|
12
13
|
|
|
13
14
|
const ModalContainer: React.FC<ModalContainerProps> = ({ isVisible, onClose, children }) => {
|
|
14
15
|
const fadeAnim = useRef(new Animated.Value(0)).current
|
|
16
|
+
const translateY = useRef(new Animated.Value(0)).current
|
|
15
17
|
const [animatedFinished, setAnimatedFinished] = useState(false)
|
|
16
18
|
|
|
19
|
+
const SWIPE_THRESHOLD = 100 // Distance to trigger close
|
|
20
|
+
const MAX_SWIPE_DISTANCE = 200 // Maximum swipe distance
|
|
21
|
+
|
|
22
|
+
const animateClose = () => {
|
|
23
|
+
Animated.parallel([
|
|
24
|
+
Animated.timing(fadeAnim, {
|
|
25
|
+
toValue: 0,
|
|
26
|
+
duration: 250,
|
|
27
|
+
useNativeDriver: true
|
|
28
|
+
}),
|
|
29
|
+
Animated.timing(translateY, {
|
|
30
|
+
toValue: MODAL_HEIGHT,
|
|
31
|
+
duration: 250,
|
|
32
|
+
useNativeDriver: true
|
|
33
|
+
})
|
|
34
|
+
]).start(() => {
|
|
35
|
+
onClose()
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
17
39
|
useEffect(() => {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
40
|
+
if (isVisible) {
|
|
41
|
+
// Start from bottom and animate to position
|
|
42
|
+
translateY.setValue(MODAL_HEIGHT)
|
|
43
|
+
Animated.parallel([
|
|
44
|
+
Animated.timing(fadeAnim, {
|
|
45
|
+
toValue: 1,
|
|
46
|
+
duration: 300,
|
|
47
|
+
useNativeDriver: true
|
|
48
|
+
}),
|
|
49
|
+
Animated.timing(translateY, {
|
|
50
|
+
toValue: 0,
|
|
51
|
+
duration: 300,
|
|
52
|
+
useNativeDriver: true
|
|
53
|
+
})
|
|
54
|
+
]).start()
|
|
55
|
+
}
|
|
56
|
+
}, [isVisible, fadeAnim, translateY])
|
|
57
|
+
|
|
58
|
+
const panGesture = Gesture.Pan()
|
|
59
|
+
.onUpdate((event) => {
|
|
60
|
+
translateY.setValue(event.translationY)
|
|
61
|
+
})
|
|
62
|
+
.onEnd((event) => {
|
|
63
|
+
const { translationY, velocityY } = event
|
|
64
|
+
|
|
65
|
+
// If swiped down with sufficient distance or velocity, close modal
|
|
66
|
+
if (translationY > SWIPE_THRESHOLD || velocityY > 500) {
|
|
67
|
+
animateClose()
|
|
68
|
+
} else {
|
|
69
|
+
// Snap back to original position
|
|
70
|
+
Animated.spring(translateY, {
|
|
71
|
+
toValue: 0,
|
|
72
|
+
useNativeDriver: true,
|
|
73
|
+
tension: 100,
|
|
74
|
+
friction: 8
|
|
75
|
+
}).start()
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
.activeOffsetY(10)
|
|
79
|
+
.runOnJS(true)
|
|
24
80
|
|
|
25
81
|
return (
|
|
26
82
|
<>
|
|
27
83
|
{isVisible && (
|
|
28
84
|
<Animated.View style={{ ...styles.backdrop, opacity: fadeAnim }}>
|
|
29
|
-
<Pressable style={styles.backdropPressable} onPress={
|
|
85
|
+
<Pressable style={styles.backdropPressable} onPress={animateClose} />
|
|
30
86
|
</Animated.View>
|
|
31
87
|
)}
|
|
32
|
-
<Modal visible={isVisible} transparent animationType='
|
|
33
|
-
<
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
88
|
+
<Modal visible={isVisible} transparent animationType='none' onRequestClose={onClose}>
|
|
89
|
+
<GestureHandlerRootView style={{ flex: 1 }}>
|
|
90
|
+
<GestureDetector gesture={panGesture}>
|
|
91
|
+
<Animated.View style={[styles.modal, { transform: [{ translateY }] }]}>
|
|
92
|
+
<SafeAreaView style={styles.safeArea}>{children}</SafeAreaView>
|
|
93
|
+
</Animated.View>
|
|
94
|
+
</GestureDetector>
|
|
95
|
+
</GestureHandlerRootView>
|
|
37
96
|
</Modal>
|
|
38
97
|
</>
|
|
39
98
|
)
|
|
@@ -51,29 +110,18 @@ const styles = StyleSheet.create({
|
|
|
51
110
|
backdropPressable: {
|
|
52
111
|
flex: 1
|
|
53
112
|
},
|
|
113
|
+
safeArea: {
|
|
114
|
+
flex: 1
|
|
115
|
+
},
|
|
54
116
|
modal: {
|
|
55
117
|
position: 'absolute',
|
|
56
118
|
bottom: 0,
|
|
57
119
|
left: 0,
|
|
58
120
|
right: 0,
|
|
59
|
-
height:
|
|
121
|
+
height: 'auto',
|
|
60
122
|
backgroundColor: 'white',
|
|
61
123
|
borderTopLeftRadius: 20,
|
|
62
|
-
borderTopRightRadius: 20
|
|
63
|
-
elevation: 10,
|
|
64
|
-
shadowColor: '#000',
|
|
65
|
-
shadowOffset: { width: 0, height: -2 },
|
|
66
|
-
shadowOpacity: 0.25,
|
|
67
|
-
shadowRadius: 4
|
|
68
|
-
},
|
|
69
|
-
modalHandle: {
|
|
70
|
-
width: 40,
|
|
71
|
-
height: 4,
|
|
72
|
-
backgroundColor: '#D1D5DB',
|
|
73
|
-
borderRadius: 2,
|
|
74
|
-
alignSelf: 'center',
|
|
75
|
-
marginTop: 8,
|
|
76
|
-
marginBottom: 8
|
|
124
|
+
borderTopRightRadius: 20
|
|
77
125
|
}
|
|
78
126
|
})
|
|
79
127
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { View, Pressable, Linking } from 'react-native'
|
|
3
|
+
import { sharedStyles } from './styles'
|
|
4
|
+
import { LogoIcon } from './icons'
|
|
5
|
+
|
|
6
|
+
interface ModalHeaderProps {
|
|
7
|
+
children?: React.ReactNode
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const ModalHeader: React.FC<ModalHeaderProps> = ({ children }) => {
|
|
11
|
+
return (
|
|
12
|
+
<View style={sharedStyles.popoverHeader}>
|
|
13
|
+
<View style={sharedStyles.modalHandle} />
|
|
14
|
+
<View style={sharedStyles.popoverHeaderContent}>
|
|
15
|
+
<Pressable onPress={() => Linking.openURL('https://www.multiplayer.app')}>
|
|
16
|
+
<LogoIcon size={42} />
|
|
17
|
+
</Pressable>
|
|
18
|
+
{children}
|
|
19
|
+
</View>
|
|
20
|
+
</View>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default ModalHeader
|
|
@@ -41,3 +41,12 @@ export const CheckmarkIcon: React.FC<IconProps> = ({ size = 24, color = 'white'
|
|
|
41
41
|
/>
|
|
42
42
|
</Svg>
|
|
43
43
|
)
|
|
44
|
+
|
|
45
|
+
export const LogoIcon: React.FC<IconProps> = ({ size = 30, color = '#473CFB' }) => (
|
|
46
|
+
<Svg width={size} height={size * 0.8} viewBox='0 0 30 24' fill='none'>
|
|
47
|
+
<Path
|
|
48
|
+
d='M28.8324 8.83643L23.5495 6.85854L21.4856 6.08553L23.6001 0.4375L15 3.65769L6.39963 0.4375L8.51441 6.08585L6.45046 6.85885L1.16757 8.83674L0.625 9.03974L9.10095 12.0981C10.0891 12.4548 10.9201 13.1265 11.4758 13.9952C11.6632 14.2883 11.8194 14.6036 11.9398 14.9369L15 23.4076L18.0602 14.9369C18.1806 14.6036 18.3368 14.288 18.5242 13.9952C19.0802 13.1265 19.9112 12.4545 20.8991 12.0981L29.375 9.03974L28.8324 8.83674V8.83643ZM19.779 10.6434C18.2872 11.1816 17.1126 12.3563 16.5744 13.848L15.014 18.173L11.5182 8.83643L10.7776 6.85854L10.2456 5.43757L9.57367 3.64272L12.3068 4.66612L18.1631 6.85885L20.8233 7.85481L23.4457 8.83674L24.104 9.08308L19.779 10.6438V10.6434Z'
|
|
49
|
+
fill={color}
|
|
50
|
+
/>
|
|
51
|
+
</Svg>
|
|
52
|
+
)
|
|
@@ -4,31 +4,37 @@ export const sharedStyles = StyleSheet.create({
|
|
|
4
4
|
// Popover styles
|
|
5
5
|
popoverContent: {
|
|
6
6
|
flex: 1,
|
|
7
|
-
paddingHorizontal:
|
|
7
|
+
paddingHorizontal: 0
|
|
8
8
|
},
|
|
9
9
|
popoverHeader: {
|
|
10
|
+
flexDirection: 'column',
|
|
11
|
+
paddingBottom: 8,
|
|
12
|
+
paddingHorizontal: 16,
|
|
13
|
+
borderTopLeftRadius: 20,
|
|
14
|
+
borderTopRightRadius: 20,
|
|
15
|
+
backgroundColor: '#e3ecfd',
|
|
16
|
+
shadowColor: '#e3ecfd',
|
|
17
|
+
shadowOffset: { width: 0, height: 10, },
|
|
18
|
+
shadowOpacity: 1,
|
|
19
|
+
shadowRadius: 5,
|
|
20
|
+
elevation: 3,
|
|
21
|
+
},
|
|
22
|
+
modalHandle: {
|
|
23
|
+
marginTop: 8,
|
|
24
|
+
marginBottom: 16,
|
|
25
|
+
width: 40,
|
|
26
|
+
height: 4,
|
|
27
|
+
backgroundColor: '#D1D5DB',
|
|
28
|
+
borderRadius: 2,
|
|
29
|
+
alignSelf: 'center',
|
|
30
|
+
},
|
|
31
|
+
popoverHeaderContent: {
|
|
10
32
|
flexDirection: 'row',
|
|
11
33
|
justifyContent: 'space-between',
|
|
12
|
-
alignItems: '
|
|
13
|
-
paddingVertical: 16,
|
|
14
|
-
borderBottomWidth: 1,
|
|
15
|
-
borderBottomColor: '#E5E7EB'
|
|
16
|
-
},
|
|
17
|
-
logoText: {
|
|
18
|
-
fontSize: 18,
|
|
19
|
-
fontWeight: 'bold',
|
|
20
|
-
color: '#007AFF'
|
|
21
|
-
},
|
|
22
|
-
closeButton: {
|
|
23
|
-
width: 30,
|
|
24
|
-
height: 30,
|
|
25
|
-
justifyContent: 'center',
|
|
26
|
-
alignItems: 'center'
|
|
27
|
-
},
|
|
28
|
-
closeButtonText: {
|
|
29
|
-
fontSize: 24,
|
|
30
|
-
color: '#6B7280'
|
|
34
|
+
alignItems: 'flex-start',
|
|
31
35
|
},
|
|
36
|
+
|
|
37
|
+
|
|
32
38
|
cancelButton: {
|
|
33
39
|
paddingHorizontal: 12,
|
|
34
40
|
paddingVertical: 6,
|
|
@@ -42,34 +48,34 @@ export const sharedStyles = StyleSheet.create({
|
|
|
42
48
|
},
|
|
43
49
|
popoverBody: {
|
|
44
50
|
flex: 1,
|
|
45
|
-
|
|
51
|
+
padding: 16,
|
|
52
|
+
paddingTop: 32,
|
|
46
53
|
},
|
|
47
54
|
title: {
|
|
48
|
-
fontSize:
|
|
49
|
-
fontWeight: '
|
|
50
|
-
color: '#
|
|
55
|
+
fontSize: 18,
|
|
56
|
+
fontWeight: '600',
|
|
57
|
+
color: '#2d3748',
|
|
51
58
|
marginBottom: 12
|
|
52
59
|
},
|
|
53
60
|
description: {
|
|
54
|
-
fontSize:
|
|
55
|
-
color: '#
|
|
56
|
-
lineHeight: 24,
|
|
61
|
+
fontSize: 14,
|
|
62
|
+
color: '#718096',
|
|
57
63
|
marginBottom: 24
|
|
58
64
|
},
|
|
59
65
|
popoverFooter: {
|
|
60
66
|
marginTop: 'auto',
|
|
61
|
-
paddingTop:
|
|
67
|
+
paddingTop: 48
|
|
62
68
|
},
|
|
63
69
|
actionButton: {
|
|
64
70
|
paddingVertical: 16,
|
|
65
71
|
paddingHorizontal: 24,
|
|
66
|
-
borderRadius:
|
|
72
|
+
borderRadius: 12,
|
|
67
73
|
alignItems: 'center'
|
|
68
74
|
},
|
|
69
75
|
actionButtonText: {
|
|
70
76
|
color: 'white',
|
|
71
77
|
fontSize: 16,
|
|
72
|
-
fontWeight: '
|
|
78
|
+
fontWeight: '500'
|
|
73
79
|
},
|
|
74
80
|
|
|
75
81
|
// Continuous recording styles
|
|
@@ -80,8 +86,15 @@ export const sharedStyles = StyleSheet.create({
|
|
|
80
86
|
marginBottom: 20,
|
|
81
87
|
paddingVertical: 12,
|
|
82
88
|
paddingHorizontal: 16,
|
|
83
|
-
|
|
84
|
-
|
|
89
|
+
borderWidth: 1,
|
|
90
|
+
borderColor: '#e1e8f1',
|
|
91
|
+
backgroundColor: '#fff',
|
|
92
|
+
borderRadius: 12,
|
|
93
|
+
shadowColor: '#000',
|
|
94
|
+
shadowOffset: { width: 0, height: 2 },
|
|
95
|
+
shadowOpacity: 0.06,
|
|
96
|
+
shadowRadius: 3,
|
|
97
|
+
elevation: 3,
|
|
85
98
|
},
|
|
86
99
|
continuousRecordingLabel: {
|
|
87
100
|
fontSize: 16,
|
|
@@ -126,12 +139,12 @@ export const sharedStyles = StyleSheet.create({
|
|
|
126
139
|
|
|
127
140
|
// Button color variants
|
|
128
141
|
startButton: {
|
|
129
|
-
backgroundColor: '#
|
|
142
|
+
backgroundColor: '#473cfb'
|
|
130
143
|
},
|
|
131
144
|
stopButton: {
|
|
132
|
-
backgroundColor: '#
|
|
145
|
+
backgroundColor: '#473cfb'
|
|
133
146
|
},
|
|
134
147
|
saveButton: {
|
|
135
|
-
backgroundColor: '#
|
|
148
|
+
backgroundColor: '#473cfb'
|
|
136
149
|
}
|
|
137
150
|
})
|
package/src/components/index.ts
CHANGED
package/src/config/defaults.ts
CHANGED
package/src/config/masking.ts
CHANGED
|
@@ -23,5 +23,6 @@ export const getMaskingConfig = (masking?: MaskingConfig): MaskingConfig => {
|
|
|
23
23
|
isContentMaskingEnabled: isValidBoolean(masking.isContentMaskingEnabled, baseMasking.isContentMaskingEnabled ?? true),
|
|
24
24
|
maskBody: isValidFunction(masking.maskBody, mask(maskBodyFieldsList)),
|
|
25
25
|
maskHeaders: isValidFunction(masking.maskHeaders, mask(maskHeadersList)),
|
|
26
|
+
inputMasking: isValidBoolean(masking.inputMasking, baseMasking.inputMasking ?? true),
|
|
26
27
|
}
|
|
27
28
|
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { NativeModules } from 'react-native'
|
|
2
|
+
|
|
3
|
+
export interface MaskingOptions {
|
|
4
|
+
/** Quality of the captured image (0.1 to 1.0) */
|
|
5
|
+
quality?: number
|
|
6
|
+
/** Whether to mask all input fields automatically */
|
|
7
|
+
inputMasking?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
export interface SessionRecorderNativeModule {
|
|
12
|
+
/**
|
|
13
|
+
* Capture the current screen and apply masking to sensitive elements
|
|
14
|
+
* @returns Promise that resolves to base64 encoded image
|
|
15
|
+
*/
|
|
16
|
+
captureAndMask(): Promise<string>
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Capture the current screen and apply masking with custom options
|
|
20
|
+
* @param options Custom masking options
|
|
21
|
+
* @returns Promise that resolves to base64 encoded image
|
|
22
|
+
*/
|
|
23
|
+
captureAndMaskWithOptions(options: MaskingOptions): Promise<string>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Get the native module
|
|
27
|
+
const { SessionRecorder } = NativeModules
|
|
28
|
+
|
|
29
|
+
// Validate that the native module is available
|
|
30
|
+
if (!SessionRecorder) {
|
|
31
|
+
console.warn('SessionRecorder native module is not available. Auto-linking may not have completed yet.')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default SessionRecorder as SessionRecorderNativeModule
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { NativeModules } from 'react-native'
|
|
2
|
+
|
|
3
|
+
export interface MaskingOptions {
|
|
4
|
+
/** Quality of the captured image (0.1 to 1.0) */
|
|
5
|
+
quality?: number
|
|
6
|
+
/** Whether to mask all input fields automatically */
|
|
7
|
+
inputMasking?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
export interface SessionRecorderNativeModule {
|
|
12
|
+
/**
|
|
13
|
+
* Capture the current screen and apply masking to sensitive elements
|
|
14
|
+
* @returns Promise that resolves to base64 encoded image
|
|
15
|
+
*/
|
|
16
|
+
captureAndMask(): Promise<string>
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Capture the current screen and apply masking with custom options
|
|
20
|
+
* @param options Custom masking options
|
|
21
|
+
* @returns Promise that resolves to base64 encoded image
|
|
22
|
+
*/
|
|
23
|
+
captureAndMaskWithOptions(options: MaskingOptions): Promise<string>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Get the native module
|
|
27
|
+
const { SessionRecorder } = NativeModules
|
|
28
|
+
|
|
29
|
+
// Validate that the native module is available
|
|
30
|
+
if (!SessionRecorder) {
|
|
31
|
+
console.warn('SessionRecorder native module is not available. Auto-linking may not have completed yet.')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default SessionRecorder as SessionRecorderNativeModule
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
generateScreenHash,
|
|
11
11
|
logger,
|
|
12
12
|
} from '../utils'
|
|
13
|
+
import { screenMaskingService, ScreenMaskingConfig } from '../services/screenMaskingService'
|
|
13
14
|
|
|
14
15
|
export class ScreenRecorder implements EventRecorder {
|
|
15
16
|
private config?: RecorderConfig
|
|
@@ -18,7 +19,7 @@ export class ScreenRecorder implements EventRecorder {
|
|
|
18
19
|
private captureInterval?: NodeJS.Timeout
|
|
19
20
|
private captureCount: number = 0
|
|
20
21
|
private maxCaptures: number = 100 // Limit captures to prevent memory issues
|
|
21
|
-
private captureQuality: number = 0.
|
|
22
|
+
private captureQuality: number = 0.1
|
|
22
23
|
private captureFormat: 'png' | 'jpg' = 'jpg'
|
|
23
24
|
private screenDimensions: { width: number; height: number } | null = null
|
|
24
25
|
private currentScreen: string | null = null
|
|
@@ -30,11 +31,24 @@ export class ScreenRecorder implements EventRecorder {
|
|
|
30
31
|
private enableChangeDetection: boolean = true
|
|
31
32
|
private hashSampleSize: number = 100
|
|
32
33
|
private currentImageNodeId: number | null = null
|
|
34
|
+
private maskingConfig?: ScreenMaskingConfig
|
|
33
35
|
|
|
34
36
|
init(config: RecorderConfig, eventRecorder?: EventRecorder): void {
|
|
35
37
|
this.config = config
|
|
36
38
|
this.eventRecorder = eventRecorder
|
|
37
39
|
this._getScreenDimensions()
|
|
40
|
+
|
|
41
|
+
// Initialize masking configuration
|
|
42
|
+
this.maskingConfig = {
|
|
43
|
+
enabled: true,
|
|
44
|
+
inputMasking: this.config?.masking?.inputMasking ?? true,
|
|
45
|
+
defaultOptions: {
|
|
46
|
+
quality: this.captureQuality,
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Update the masking service configuration
|
|
51
|
+
screenMaskingService.updateConfig(this.maskingConfig)
|
|
38
52
|
}
|
|
39
53
|
|
|
40
54
|
start(): void {
|
|
@@ -135,6 +149,21 @@ export class ScreenRecorder implements EventRecorder {
|
|
|
135
149
|
|
|
136
150
|
private async _captureScreenBase64(): Promise<string | null> {
|
|
137
151
|
try {
|
|
152
|
+
// Try native masking first if available
|
|
153
|
+
if (screenMaskingService.isScreenMaskingAvailable()) {
|
|
154
|
+
logger.info('ScreenRecorder', 'Using native masking for screen capture')
|
|
155
|
+
const maskedImage = await screenMaskingService.captureMaskedScreen({
|
|
156
|
+
quality: this.captureQuality,
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
if (maskedImage) {
|
|
160
|
+
return maskedImage
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
logger.warn('ScreenRecorder', 'Native masking failed, falling back to view-shot')
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Fallback to react-native-view-shot
|
|
138
167
|
if (!this.viewShotRef) {
|
|
139
168
|
logger.warn('ScreenRecorder', 'ViewShot ref not available for screen capture')
|
|
140
169
|
return null
|
|
@@ -451,7 +480,7 @@ export class ScreenRecorder implements EventRecorder {
|
|
|
451
480
|
// Get current configuration
|
|
452
481
|
getConfiguration(): Record<string, any> {
|
|
453
482
|
return {
|
|
454
|
-
captureInterval: this.captureInterval ?
|
|
483
|
+
captureInterval: this.captureInterval ? 2000 : 0, // Default 5 seconds
|
|
455
484
|
captureQuality: this.captureQuality,
|
|
456
485
|
captureFormat: this.captureFormat,
|
|
457
486
|
maxCaptures: this.maxCaptures,
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import SessionRecorderNative, { MaskingOptions } from '../native/SessionRecorderNative'
|
|
2
|
+
import { logger } from '../utils'
|
|
3
|
+
|
|
4
|
+
export interface ScreenMaskingConfig {
|
|
5
|
+
/** Whether screen masking is enabled */
|
|
6
|
+
enabled: boolean
|
|
7
|
+
/** Whether to mask all input fields automatically */
|
|
8
|
+
inputMasking: boolean
|
|
9
|
+
/** Default masking options */
|
|
10
|
+
defaultOptions?: MaskingOptions
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class ScreenMaskingService {
|
|
14
|
+
private config: ScreenMaskingConfig
|
|
15
|
+
private isAvailable: boolean = false
|
|
16
|
+
|
|
17
|
+
constructor(config: ScreenMaskingConfig = { enabled: true, inputMasking: true }) {
|
|
18
|
+
this.config = config
|
|
19
|
+
this.checkAvailability()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if the native masking module is available
|
|
24
|
+
*/
|
|
25
|
+
private checkAvailability(): void {
|
|
26
|
+
try {
|
|
27
|
+
// Try to access the native module to check if it's available
|
|
28
|
+
if (SessionRecorderNative && typeof SessionRecorderNative.captureAndMask === 'function') {
|
|
29
|
+
this.isAvailable = true
|
|
30
|
+
logger.info('ScreenMaskingService', 'Screen masking native module is available')
|
|
31
|
+
} else {
|
|
32
|
+
this.isAvailable = false
|
|
33
|
+
logger.warn('ScreenMaskingService', 'Screen masking native module is not available - auto-linking may still be in progress')
|
|
34
|
+
|
|
35
|
+
// Retry after a delay for auto-linking
|
|
36
|
+
setTimeout(() => {
|
|
37
|
+
this.checkAvailability()
|
|
38
|
+
}, 2000)
|
|
39
|
+
}
|
|
40
|
+
} catch (error) {
|
|
41
|
+
this.isAvailable = false
|
|
42
|
+
logger.error('ScreenMaskingService', 'Error checking screen masking availability:', error)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Capture screen with masking applied
|
|
48
|
+
*/
|
|
49
|
+
async captureMaskedScreen(options?: MaskingOptions): Promise<string | null> {
|
|
50
|
+
if (!this.isAvailable || !this.config.enabled) {
|
|
51
|
+
logger.warn('ScreenMaskingService', 'Screen masking is not available or disabled')
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const maskingOptions: MaskingOptions = {
|
|
57
|
+
...this.config.defaultOptions,
|
|
58
|
+
...options,
|
|
59
|
+
inputMasking: this.config.inputMasking,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const maskedImageBase64 = await SessionRecorderNative.captureAndMaskWithOptions(maskingOptions)
|
|
63
|
+
logger.info('ScreenMaskingService', 'Successfully captured masked screen')
|
|
64
|
+
return maskedImageBase64
|
|
65
|
+
} catch (error) {
|
|
66
|
+
logger.error('ScreenMaskingService', 'Failed to capture masked screen:', error)
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Capture screen with basic masking (no custom options)
|
|
73
|
+
*/
|
|
74
|
+
async captureMaskedScreenBasic(): Promise<string | null> {
|
|
75
|
+
if (!this.isAvailable || !this.config.enabled) {
|
|
76
|
+
logger.warn('ScreenMaskingService', 'Screen masking is not available or disabled')
|
|
77
|
+
return null
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const maskedImageBase64 = await SessionRecorderNative.captureAndMask()
|
|
82
|
+
logger.info('ScreenMaskingService', 'Successfully captured masked screen (basic)')
|
|
83
|
+
return maskedImageBase64
|
|
84
|
+
} catch (error) {
|
|
85
|
+
logger.error('ScreenMaskingService', 'Failed to capture masked screen (basic):', error)
|
|
86
|
+
return null
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Update the masking configuration
|
|
92
|
+
*/
|
|
93
|
+
updateConfig(config: Partial<ScreenMaskingConfig>): void {
|
|
94
|
+
this.config = { ...this.config, ...config }
|
|
95
|
+
logger.info('ScreenMaskingService', 'Screen masking configuration updated')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Check if screen masking is available
|
|
100
|
+
*/
|
|
101
|
+
isScreenMaskingAvailable(): boolean {
|
|
102
|
+
return this.isAvailable && this.config.enabled
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get the current configuration
|
|
107
|
+
*/
|
|
108
|
+
getConfig(): ScreenMaskingConfig {
|
|
109
|
+
return { ...this.config }
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Create a singleton instance
|
|
114
|
+
export const screenMaskingService = new ScreenMaskingService()
|
|
@@ -150,7 +150,6 @@ export interface MaskingConfig {
|
|
|
150
150
|
/** Custom function for masking headers in traces */
|
|
151
151
|
maskHeaders?: (headers: any, span: any) => any;
|
|
152
152
|
|
|
153
|
-
|
|
154
153
|
/** List of body fields to mask in traces */
|
|
155
154
|
maskBodyFieldsList?: string[]
|
|
156
155
|
/** List of headers to mask in traces */
|
|
@@ -160,6 +159,11 @@ export interface MaskingConfig {
|
|
|
160
159
|
headersToInclude?: string[]
|
|
161
160
|
/** List of headers to exclude from traces */
|
|
162
161
|
headersToExclude?: string[]
|
|
162
|
+
|
|
163
|
+
/** Whether to mask all input fields during screen recording
|
|
164
|
+
* @default true
|
|
165
|
+
*/
|
|
166
|
+
inputMasking?: boolean
|
|
163
167
|
}
|
|
164
168
|
|
|
165
169
|
/**
|
|
@@ -222,6 +226,8 @@ export interface RecorderConfig extends BaseConfig {
|
|
|
222
226
|
recordNavigation?: boolean
|
|
223
227
|
/** Whether to record screen */
|
|
224
228
|
recordScreen?: boolean
|
|
229
|
+
/** Configuration for masking sensitive data in session recordings */
|
|
230
|
+
masking?: MaskingConfig
|
|
225
231
|
}
|
|
226
232
|
|
|
227
233
|
/**
|