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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/android/build.gradle +32 -0
  2. package/android/src/main/AndroidManifest.xml +2 -0
  3. package/android/src/main/java/com/multiplayer/sessionrecorder/ScreenMaskingModule.kt +202 -0
  4. package/android/src/main/java/com/multiplayer/sessionrecorder/ScreenMaskingPackage.kt +16 -0
  5. package/android/src/main/java/com/multiplayer/sessionrecorder/SessionRecorderModule.kt +202 -0
  6. package/android/src/main/java/com/multiplayer/sessionrecorder/SessionRecorderPackage.kt +16 -0
  7. package/dist/components/MaskableComponent.d.ts +22 -0
  8. package/dist/components/MaskableComponent.js +1 -0
  9. package/dist/components/MaskableComponent.js.map +1 -0
  10. package/dist/components/MaskableTextInput.d.ts +14 -0
  11. package/dist/components/MaskableTextInput.js +1 -0
  12. package/dist/components/MaskableTextInput.js.map +1 -0
  13. package/dist/components/SessionRecorderWidget/FinalPopover.js +1 -1
  14. package/dist/components/SessionRecorderWidget/FinalPopover.js.map +1 -1
  15. package/dist/components/SessionRecorderWidget/FloatingButton.js +1 -1
  16. package/dist/components/SessionRecorderWidget/FloatingButton.js.map +1 -1
  17. package/dist/components/SessionRecorderWidget/InitialPopover.js +1 -1
  18. package/dist/components/SessionRecorderWidget/InitialPopover.js.map +1 -1
  19. package/dist/components/SessionRecorderWidget/ModalContainer.js +1 -1
  20. package/dist/components/SessionRecorderWidget/ModalContainer.js.map +1 -1
  21. package/dist/components/SessionRecorderWidget/ModalHeader.d.ts +6 -0
  22. package/dist/components/SessionRecorderWidget/ModalHeader.js +1 -0
  23. package/dist/components/SessionRecorderWidget/ModalHeader.js.map +1 -0
  24. package/dist/components/SessionRecorderWidget/icons.d.ts +1 -0
  25. package/dist/components/SessionRecorderWidget/icons.js +1 -1
  26. package/dist/components/SessionRecorderWidget/icons.js.map +1 -1
  27. package/dist/components/SessionRecorderWidget/styles.d.ts +39 -22
  28. package/dist/components/SessionRecorderWidget/styles.js +1 -1
  29. package/dist/components/SessionRecorderWidget/styles.js.map +1 -1
  30. package/dist/components/index.d.ts +2 -0
  31. package/dist/components/index.js +1 -1
  32. package/dist/components/index.js.map +1 -1
  33. package/dist/config/defaults.js +1 -1
  34. package/dist/config/defaults.js.map +1 -1
  35. package/dist/config/masking.js +1 -1
  36. package/dist/config/masking.js.map +1 -1
  37. package/dist/native/ScreenMasking.d.ts +21 -0
  38. package/dist/native/ScreenMasking.js +1 -0
  39. package/dist/native/ScreenMasking.js.map +1 -0
  40. package/dist/native/SessionRecorderNative.d.ts +21 -0
  41. package/dist/native/SessionRecorderNative.js +1 -0
  42. package/dist/native/SessionRecorderNative.js.map +1 -0
  43. package/dist/recorder/screenRecorder.d.ts +1 -0
  44. package/dist/recorder/screenRecorder.js +1 -1
  45. package/dist/recorder/screenRecorder.js.map +1 -1
  46. package/dist/recorder/screenshotManager.d.ts +10 -0
  47. package/dist/recorder/screenshotManager.js +1 -0
  48. package/dist/recorder/screenshotManager.js.map +1 -0
  49. package/dist/services/screenMaskingService.d.ts +39 -0
  50. package/dist/services/screenMaskingService.js +1 -0
  51. package/dist/services/screenMaskingService.js.map +1 -0
  52. package/dist/types/session-recorder.d.ts +6 -0
  53. package/dist/types/session-recorder.js.map +1 -1
  54. package/dist/utils/componentRegistry.d.ts +64 -0
  55. package/dist/utils/componentRegistry.js +1 -0
  56. package/dist/utils/componentRegistry.js.map +1 -0
  57. package/dist/utils/nativeModuleTest.d.ts +8 -0
  58. package/dist/utils/nativeModuleTest.js +1 -0
  59. package/dist/utils/nativeModuleTest.js.map +1 -0
  60. package/dist/utils/reactNativeHierarchyExtractor.d.ts +38 -0
  61. package/dist/utils/reactNativeHierarchyExtractor.js +1 -0
  62. package/dist/utils/reactNativeHierarchyExtractor.js.map +1 -0
  63. package/dist/utils/screenshotMasker.d.ts +96 -0
  64. package/dist/utils/screenshotMasker.js +1 -0
  65. package/dist/utils/screenshotMasker.js.map +1 -0
  66. package/dist/utils/viewHierarchyTracker.d.ts +89 -0
  67. package/dist/utils/viewHierarchyTracker.js +1 -0
  68. package/dist/utils/viewHierarchyTracker.js.map +1 -0
  69. package/docs/TROUBLESHOOTING.md +168 -0
  70. package/ios/ScreenMasking.m +12 -0
  71. package/ios/ScreenMasking.podspec +21 -0
  72. package/ios/ScreenMasking.swift +205 -0
  73. package/ios/SessionRecorder.m +12 -0
  74. package/ios/SessionRecorder.podspec +21 -0
  75. package/ios/SessionRecorder.swift +205 -0
  76. package/ios/SessionRecorderNative.podspec +21 -0
  77. package/package.json +10 -1
  78. package/react-native.config.js +15 -0
  79. package/src/components/SessionRecorderWidget/FinalPopover.tsx +5 -16
  80. package/src/components/SessionRecorderWidget/FloatingButton.tsx +14 -27
  81. package/src/components/SessionRecorderWidget/InitialPopover.tsx +3 -9
  82. package/src/components/SessionRecorderWidget/ModalContainer.tsx +77 -29
  83. package/src/components/SessionRecorderWidget/ModalHeader.tsx +24 -0
  84. package/src/components/SessionRecorderWidget/icons.tsx +9 -0
  85. package/src/components/SessionRecorderWidget/styles.ts +48 -35
  86. package/src/components/index.ts +3 -1
  87. package/src/config/defaults.ts +1 -0
  88. package/src/config/masking.ts +1 -0
  89. package/src/native/ScreenMasking.ts +34 -0
  90. package/src/native/SessionRecorderNative.ts +34 -0
  91. package/src/recorder/screenRecorder.ts +31 -2
  92. package/src/services/screenMaskingService.ts +118 -0
  93. package/src/types/session-recorder.ts +7 -1
  94. package/src/utils/nativeModuleTest.ts +60 -0
@@ -0,0 +1,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/SessionRecorderNative.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 && (
@@ -1,5 +1,6 @@
1
1
  import React, { useEffect, useRef, useState } from 'react'
2
- import { Animated, View, Pressable, StyleSheet, Dimensions, Modal } from 'react-native'
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
- Animated.timing(fadeAnim, {
19
- toValue: isVisible ? 1 : 0,
20
- duration: 300,
21
- useNativeDriver: true
22
- }).start()
23
- }, [isVisible, fadeAnim])
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={onClose} />
85
+ <Pressable style={styles.backdropPressable} onPress={animateClose} />
30
86
  </Animated.View>
31
87
  )}
32
- <Modal visible={isVisible} transparent animationType='slide' onRequestClose={onClose}>
33
- <View style={styles.modal}>
34
- <View style={styles.modalHandle} />
35
- {children}
36
- </View>
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: MODAL_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: 20
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: 'center',
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
- paddingVertical: 20
51
+ padding: 16,
52
+ paddingTop: 32,
46
53
  },
47
54
  title: {
48
- fontSize: 24,
49
- fontWeight: 'bold',
50
- color: '#111827',
55
+ fontSize: 18,
56
+ fontWeight: '600',
57
+ color: '#2d3748',
51
58
  marginBottom: 12
52
59
  },
53
60
  description: {
54
- fontSize: 16,
55
- color: '#6B7280',
56
- lineHeight: 24,
61
+ fontSize: 14,
62
+ color: '#718096',
57
63
  marginBottom: 24
58
64
  },
59
65
  popoverFooter: {
60
66
  marginTop: 'auto',
61
- paddingTop: 20
67
+ paddingTop: 48
62
68
  },
63
69
  actionButton: {
64
70
  paddingVertical: 16,
65
71
  paddingHorizontal: 24,
66
- borderRadius: 8,
72
+ borderRadius: 12,
67
73
  alignItems: 'center'
68
74
  },
69
75
  actionButtonText: {
70
76
  color: 'white',
71
77
  fontSize: 16,
72
- fontWeight: '600'
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
- backgroundColor: '#F9FAFB',
84
- borderRadius: 8
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: '#007AFF'
142
+ backgroundColor: '#473cfb'
130
143
  },
131
144
  stopButton: {
132
- backgroundColor: '#FF4444'
145
+ backgroundColor: '#473cfb'
133
146
  },
134
147
  saveButton: {
135
- backgroundColor: '#34C759'
148
+ backgroundColor: '#473cfb'
136
149
  }
137
150
  })
@@ -1 +1,3 @@
1
- export * from './GestureCaptureWrapper'
1
+ export * from './GestureCaptureWrapper'
2
+ export * from './ScreenRecorderView'
3
+ export * from './SessionRecorderWidget'
@@ -23,6 +23,7 @@ export const DEFAULT_MASKING_CONFIG: MaskingConfig = {
23
23
  maskHeadersList: sensitiveHeaders,
24
24
  headersToInclude: [],
25
25
  headersToExclude: [],
26
+ inputMasking: true,
26
27
  }
27
28
 
28
29
  export const DEFAULT_WIDGET_TEXT_CONFIG: WidgetTextOverridesConfig = {
@@ -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 { SessionRecorderNative } = NativeModules
28
+
29
+ // Validate that the native module is available
30
+ if (!SessionRecorderNative) {
31
+ console.warn('SessionRecorderNative module is not available. Auto-linking may not have completed yet.')
32
+ }
33
+
34
+ export default SessionRecorderNative 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.3
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 ? 5000 : 0, // Default 5 seconds
483
+ captureInterval: this.captureInterval ? 2000 : 0, // Default 5 seconds
455
484
  captureQuality: this.captureQuality,
456
485
  captureFormat: this.captureFormat,
457
486
  maxCaptures: this.maxCaptures,