@multiplayer-app/session-recorder-react-native 0.0.1-beta.3 → 0.0.1-beta.4
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/dist/components/SessionRecorderWidget/FinalPopover.d.ts +11 -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 +13 -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/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 +10 -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 +128 -0
- package/dist/components/SessionRecorderWidget/styles.js +1 -0
- package/dist/components/SessionRecorderWidget/styles.js.map +1 -0
- package/dist/context/SessionRecorderContext.d.ts +5 -3
- package/dist/context/SessionRecorderContext.js +1 -1
- package/dist/context/SessionRecorderContext.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/patch/xhr.d.ts +1 -1
- package/dist/patch/xhr.js +1 -1
- package/dist/patch/xhr.js.map +1 -1
- 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 +2 -1
- package/dist/session-recorder.js +1 -1
- package/dist/session-recorder.js.map +1 -1
- package/dist/utils/platform.d.ts +3 -0
- package/package.json +3 -2
- package/src/components/SessionRecorderWidget/FinalPopover.tsx +73 -0
- package/src/components/SessionRecorderWidget/FloatingButton.tsx +149 -0
- package/src/components/SessionRecorderWidget/InitialPopover.tsx +95 -0
- package/src/components/SessionRecorderWidget/ModalContainer.tsx +80 -0
- package/src/components/SessionRecorderWidget/SessionRecorderWidget.tsx +109 -0
- package/src/components/SessionRecorderWidget/icons.tsx +43 -0
- package/src/components/SessionRecorderWidget/index.ts +3 -0
- package/src/components/SessionRecorderWidget/styles.ts +137 -0
- package/src/context/SessionRecorderContext.tsx +12 -34
- package/src/index.ts +1 -9
- package/src/patch/xhr.ts +7 -7
- package/src/services/storage.service.ts +45 -4
- package/src/session-recorder.ts +9 -3
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import React, { useMemo, useState } from 'react'
|
|
2
|
+
import { Alert } from 'react-native'
|
|
3
|
+
import { SessionState } from '../../types'
|
|
4
|
+
import { SessionType } from '@multiplayer-app/session-recorder-common'
|
|
5
|
+
import { useSessionRecorder } from '../../context/SessionRecorderContext'
|
|
6
|
+
import FloatingButton from './FloatingButton'
|
|
7
|
+
import ModalContainer from './ModalContainer'
|
|
8
|
+
import InitialPopover from './InitialPopover'
|
|
9
|
+
import FinalPopover from './FinalPopover'
|
|
10
|
+
|
|
11
|
+
interface SessionRecorderWidgetProps {}
|
|
12
|
+
|
|
13
|
+
const SessionRecorderWidget: React.FC<SessionRecorderWidgetProps> = () => {
|
|
14
|
+
const { sessionState, instance } = useSessionRecorder()
|
|
15
|
+
const [isModalVisible, setIsModalVisible] = useState(false)
|
|
16
|
+
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
17
|
+
|
|
18
|
+
// Get configuration from instance
|
|
19
|
+
const config = instance.config
|
|
20
|
+
const textOverrides = config.widgetTextOverrides
|
|
21
|
+
const showContinuousRecording = config.showContinuousRecording
|
|
22
|
+
|
|
23
|
+
const openModal = () => {
|
|
24
|
+
setIsModalVisible(true)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const closeModal = () => {
|
|
28
|
+
setIsModalVisible(false)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const onStartRecording = async (sessionType: SessionType) => {
|
|
32
|
+
try {
|
|
33
|
+
instance.start(sessionType)
|
|
34
|
+
closeModal()
|
|
35
|
+
} catch (error) {
|
|
36
|
+
Alert.alert('Error', 'Failed to start recording')
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const onStopRecording = async (comment: string) => {
|
|
41
|
+
try {
|
|
42
|
+
setIsSubmitting(true)
|
|
43
|
+
await instance.stop(comment)
|
|
44
|
+
closeModal()
|
|
45
|
+
Alert.alert('Success', 'Session saved successfully')
|
|
46
|
+
} catch (error) {
|
|
47
|
+
Alert.alert('Error', 'Failed to save session')
|
|
48
|
+
} finally {
|
|
49
|
+
setIsSubmitting(false)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const onCancelSession = async () => {
|
|
54
|
+
try {
|
|
55
|
+
await instance.cancel()
|
|
56
|
+
closeModal()
|
|
57
|
+
} catch (error) {
|
|
58
|
+
Alert.alert('Error', 'Failed to cancel session')
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const onSaveContinuousSession = async () => {
|
|
63
|
+
try {
|
|
64
|
+
setIsSubmitting(true)
|
|
65
|
+
await instance.save()
|
|
66
|
+
closeModal()
|
|
67
|
+
Alert.alert('Success', 'Continuous session saved successfully')
|
|
68
|
+
} catch (error) {
|
|
69
|
+
Alert.alert('Error', 'Failed to save continuous session')
|
|
70
|
+
} finally {
|
|
71
|
+
setIsSubmitting(false)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const renderModalContent = useMemo(() => {
|
|
76
|
+
if (sessionState === SessionState.started || sessionState === SessionState.paused) {
|
|
77
|
+
return (
|
|
78
|
+
<FinalPopover
|
|
79
|
+
isSubmitting={isSubmitting}
|
|
80
|
+
textOverrides={textOverrides}
|
|
81
|
+
onClose={closeModal}
|
|
82
|
+
onStopRecording={onStopRecording}
|
|
83
|
+
onCancelSession={onCancelSession}
|
|
84
|
+
/>
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
return (
|
|
88
|
+
<InitialPopover
|
|
89
|
+
isSubmitting={isSubmitting}
|
|
90
|
+
textOverrides={textOverrides}
|
|
91
|
+
showContinuousRecording={showContinuousRecording}
|
|
92
|
+
onClose={closeModal}
|
|
93
|
+
onStartRecording={onStartRecording}
|
|
94
|
+
onSaveContinuousSession={onSaveContinuousSession}
|
|
95
|
+
/>
|
|
96
|
+
)
|
|
97
|
+
}, [sessionState, isSubmitting, textOverrides, showContinuousRecording])
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<>
|
|
101
|
+
<FloatingButton sessionState={sessionState} onPress={openModal} />
|
|
102
|
+
<ModalContainer isVisible={isModalVisible} onClose={closeModal}>
|
|
103
|
+
{renderModalContent}
|
|
104
|
+
</ModalContainer>
|
|
105
|
+
</>
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export default SessionRecorderWidget
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import Svg, { Path, Circle } from 'react-native-svg'
|
|
3
|
+
|
|
4
|
+
interface IconProps {
|
|
5
|
+
size?: number
|
|
6
|
+
color?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const RecordIcon: React.FC<IconProps> = ({ size = 19, color = 'white' }) => (
|
|
10
|
+
<Svg width={size} height={size} viewBox='0 0 19 19' fill='none'>
|
|
11
|
+
<Path
|
|
12
|
+
fillRule='evenodd'
|
|
13
|
+
clipRule='evenodd'
|
|
14
|
+
d='M6.68926 5.30356C6.56568 5.38721 6.39976 5.37561 6.29459 5.26937L3.58782 2.53477C3.46424 2.40992 3.47196 2.20492 3.60862 2.09483C5.2319 0.786982 7.28494 0 9.51866 0C11.7535 0 13.8066 0.787042 15.4308 2.09586C15.5674 2.20596 15.5752 2.41091 15.4516 2.53577L12.7468 5.26931C12.6416 5.37558 12.4757 5.38719 12.3521 5.30353C11.5393 4.75345 10.571 4.42281 9.52066 4.42281C8.47036 4.42281 7.50203 4.75346 6.68926 5.30356ZM16.4926 3.4303C16.6163 3.30527 16.8197 3.31303 16.9288 3.45121C18.2224 5.08933 19.0001 7.15932 19.0001 9.4116C19.0001 11.6671 18.2204 13.7392 16.9238 15.3785C16.8147 15.5165 16.6114 15.5242 16.4877 15.3992L13.7872 12.6701C13.682 12.5638 13.6708 12.3962 13.7538 12.2716C14.3006 11.451 14.6291 10.4727 14.6291 9.4116C14.6291 8.35454 14.3016 7.37925 13.756 6.56083C13.6728 6.43616 13.6841 6.26857 13.7893 6.16224L16.4926 3.4303ZM5.21676 12.6712C5.322 12.5649 5.3333 12.3974 5.2502 12.2727C4.70331 11.4522 4.374 10.4737 4.374 9.41184C4.374 8.35469 4.70232 7.37949 5.24808 6.56106C5.33123 6.43637 5.31996 6.26872 5.2147 6.16241L2.50855 3.4293C2.38482 3.30434 2.18146 3.31213 2.07236 3.45028C0.77864 5.08841 0 7.15845 0 9.41184C0 11.6684 0.78066 13.7406 2.07831 15.3799C2.18749 15.5178 2.39066 15.5255 2.51429 15.4006L5.21676 12.6712ZM12.3323 13.707C12.4559 13.6231 12.6221 13.6346 12.7273 13.741L15.4277 16.4691C15.5513 16.594 15.5435 16.7991 15.4068 16.9091C13.7837 18.215 11.7327 19 9.49998 19C7.2693 19 5.21837 18.2159 3.59619 16.9102C3.45943 16.8001 3.45169 16.595 3.57533 16.4702L6.27769 13.7409C6.38296 13.6346 6.54906 13.6231 6.67267 13.707C7.48459 14.2577 8.45278 14.5883 9.50198 14.5883C10.5522 14.5883 11.5204 14.2578 12.3323 13.707Z'
|
|
15
|
+
fill={color}
|
|
16
|
+
/>
|
|
17
|
+
</Svg>
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
export const CapturingIcon: React.FC<IconProps> = ({ size = 24, color = 'white' }) => (
|
|
21
|
+
<Svg width={size} height={size} viewBox='0 0 24 24' fill='none'>
|
|
22
|
+
<Circle cx='12' cy='12' r='4' fill={color} />
|
|
23
|
+
<Circle cx='12' cy='12' r='7.5' stroke={color} strokeWidth='1' />
|
|
24
|
+
<Circle cx='12' cy='12' r='11.5' stroke={color} strokeWidth='1' opacity='0.2' />
|
|
25
|
+
</Svg>
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
export const PausedIcon: React.FC<IconProps> = ({ size = 24, color = 'white' }) => (
|
|
29
|
+
<Svg width={size} height={size} viewBox='0 0 24 24' fill='none'>
|
|
30
|
+
<Path d='M8 5V19M16 5V19' stroke={color} strokeWidth='2' strokeLinecap='round' strokeLinejoin='round' />
|
|
31
|
+
</Svg>
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
export const CheckmarkIcon: React.FC<IconProps> = ({ size = 24, color = 'white' }) => (
|
|
35
|
+
<Svg width={size} height={size} viewBox='0 0 24 24' fill='none'>
|
|
36
|
+
<Path
|
|
37
|
+
fillRule='evenodd'
|
|
38
|
+
clipRule='evenodd'
|
|
39
|
+
d='M20.0481 6.35147C20.5168 6.8201 20.5168 7.5799 20.0481 8.04853L10.4481 17.6485C9.97951 18.1172 9.21971 18.1172 8.75108 17.6485L3.95108 12.8485C3.48245 12.3799 3.48245 11.6201 3.95108 11.1515C4.41971 10.6828 5.17951 10.6828 5.64814 11.1515L9.59961 15.1029L18.3511 6.35147C18.8197 5.88284 19.5795 5.88284 20.0481 6.35147Z'
|
|
40
|
+
fill={color}
|
|
41
|
+
/>
|
|
42
|
+
</Svg>
|
|
43
|
+
)
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native'
|
|
2
|
+
|
|
3
|
+
export const sharedStyles = StyleSheet.create({
|
|
4
|
+
// Popover styles
|
|
5
|
+
popoverContent: {
|
|
6
|
+
flex: 1,
|
|
7
|
+
paddingHorizontal: 20
|
|
8
|
+
},
|
|
9
|
+
popoverHeader: {
|
|
10
|
+
flexDirection: 'row',
|
|
11
|
+
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'
|
|
31
|
+
},
|
|
32
|
+
cancelButton: {
|
|
33
|
+
paddingHorizontal: 12,
|
|
34
|
+
paddingVertical: 6,
|
|
35
|
+
borderRadius: 6,
|
|
36
|
+
backgroundColor: '#F3F4F6'
|
|
37
|
+
},
|
|
38
|
+
cancelButtonText: {
|
|
39
|
+
color: '#374151',
|
|
40
|
+
fontSize: 14,
|
|
41
|
+
fontWeight: '500'
|
|
42
|
+
},
|
|
43
|
+
popoverBody: {
|
|
44
|
+
flex: 1,
|
|
45
|
+
paddingVertical: 20
|
|
46
|
+
},
|
|
47
|
+
title: {
|
|
48
|
+
fontSize: 24,
|
|
49
|
+
fontWeight: 'bold',
|
|
50
|
+
color: '#111827',
|
|
51
|
+
marginBottom: 12
|
|
52
|
+
},
|
|
53
|
+
description: {
|
|
54
|
+
fontSize: 16,
|
|
55
|
+
color: '#6B7280',
|
|
56
|
+
lineHeight: 24,
|
|
57
|
+
marginBottom: 24
|
|
58
|
+
},
|
|
59
|
+
popoverFooter: {
|
|
60
|
+
marginTop: 'auto',
|
|
61
|
+
paddingTop: 20
|
|
62
|
+
},
|
|
63
|
+
actionButton: {
|
|
64
|
+
paddingVertical: 16,
|
|
65
|
+
paddingHorizontal: 24,
|
|
66
|
+
borderRadius: 8,
|
|
67
|
+
alignItems: 'center'
|
|
68
|
+
},
|
|
69
|
+
actionButtonText: {
|
|
70
|
+
color: 'white',
|
|
71
|
+
fontSize: 16,
|
|
72
|
+
fontWeight: '600'
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
// Continuous recording styles
|
|
76
|
+
continuousRecordingSection: {
|
|
77
|
+
flexDirection: 'row',
|
|
78
|
+
justifyContent: 'space-between',
|
|
79
|
+
alignItems: 'center',
|
|
80
|
+
marginBottom: 20,
|
|
81
|
+
paddingVertical: 12,
|
|
82
|
+
paddingHorizontal: 16,
|
|
83
|
+
backgroundColor: '#F9FAFB',
|
|
84
|
+
borderRadius: 8
|
|
85
|
+
},
|
|
86
|
+
continuousRecordingLabel: {
|
|
87
|
+
fontSize: 16,
|
|
88
|
+
fontWeight: '500',
|
|
89
|
+
color: '#374151'
|
|
90
|
+
},
|
|
91
|
+
continuousOverlay: {
|
|
92
|
+
marginTop: 20,
|
|
93
|
+
padding: 16,
|
|
94
|
+
backgroundColor: '#FEF3C7',
|
|
95
|
+
borderRadius: 8,
|
|
96
|
+
borderWidth: 1,
|
|
97
|
+
borderColor: '#F59E0B'
|
|
98
|
+
},
|
|
99
|
+
continuousOverlayContent: {
|
|
100
|
+
marginBottom: 16
|
|
101
|
+
},
|
|
102
|
+
continuousOverlayTitle: {
|
|
103
|
+
fontSize: 18,
|
|
104
|
+
fontWeight: 'bold',
|
|
105
|
+
color: '#92400E',
|
|
106
|
+
marginBottom: 8
|
|
107
|
+
},
|
|
108
|
+
continuousOverlayDescription: {
|
|
109
|
+
fontSize: 14,
|
|
110
|
+
color: '#92400E',
|
|
111
|
+
lineHeight: 20
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
// Comment input styles
|
|
115
|
+
commentInput: {
|
|
116
|
+
borderWidth: 1,
|
|
117
|
+
borderColor: '#D1D5DB',
|
|
118
|
+
borderRadius: 8,
|
|
119
|
+
padding: 12,
|
|
120
|
+
fontSize: 16,
|
|
121
|
+
color: '#374151',
|
|
122
|
+
backgroundColor: '#F9FAFB',
|
|
123
|
+
marginBottom: 24,
|
|
124
|
+
minHeight: 80
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
// Button color variants
|
|
128
|
+
startButton: {
|
|
129
|
+
backgroundColor: '#007AFF'
|
|
130
|
+
},
|
|
131
|
+
stopButton: {
|
|
132
|
+
backgroundColor: '#FF4444'
|
|
133
|
+
},
|
|
134
|
+
saveButton: {
|
|
135
|
+
backgroundColor: '#34C759'
|
|
136
|
+
}
|
|
137
|
+
})
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import React, { createContext, useContext, PropsWithChildren, useState, useEffect, useRef } from 'react'
|
|
2
|
-
import { Pressable, Text, View } from 'react-native'
|
|
3
2
|
import { SessionRecorderOptions, SessionState } from '../types'
|
|
4
|
-
import SessionRecorder from '../session-recorder'
|
|
5
3
|
import sessionRecorder from '../session-recorder'
|
|
6
4
|
import { ScreenRecorderView } from '../components/ScreenRecorderView'
|
|
5
|
+
import SessionRecorderWidget from '../components/SessionRecorderWidget'
|
|
7
6
|
|
|
8
7
|
interface SessionRecorderContextType {
|
|
9
|
-
instance: typeof
|
|
8
|
+
instance: typeof sessionRecorder
|
|
9
|
+
isInitialized: boolean
|
|
10
|
+
sessionState: SessionState | null
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
const SessionRecorderContext = createContext<SessionRecorderContextType | null>(null)
|
|
@@ -16,52 +17,29 @@ export interface SessionRecorderProviderProps extends PropsWithChildren {
|
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export const SessionRecorderProvider: React.FC<SessionRecorderProviderProps> = ({ children, options }) => {
|
|
19
|
-
const [
|
|
20
|
+
const [isInitialized, setIsInitialized] = useState(false)
|
|
21
|
+
const [sessionState, setSessionState] = useState<SessionState | null>(SessionState.stopped)
|
|
20
22
|
const optionsRef = useRef<string>()
|
|
21
23
|
|
|
22
24
|
useEffect(() => {
|
|
23
25
|
const newOptions = JSON.stringify(options)
|
|
24
26
|
if (optionsRef.current === JSON.stringify(options)) return
|
|
25
27
|
optionsRef.current = newOptions
|
|
26
|
-
|
|
28
|
+
sessionRecorder.init(options)
|
|
29
|
+
setIsInitialized(true)
|
|
27
30
|
}, [options])
|
|
28
31
|
|
|
29
32
|
useEffect(() => {
|
|
30
|
-
setSessionState(
|
|
31
|
-
|
|
33
|
+
setSessionState(sessionRecorder.sessionState)
|
|
34
|
+
sessionRecorder.on('state-change', (state: SessionState) => {
|
|
32
35
|
setSessionState(state)
|
|
33
36
|
})
|
|
34
37
|
}, [])
|
|
35
38
|
|
|
36
|
-
const onToggleSession = () => {
|
|
37
|
-
if (SessionRecorder.sessionState === SessionState.started) {
|
|
38
|
-
SessionRecorder.stop()
|
|
39
|
-
} else {
|
|
40
|
-
SessionRecorder.start()
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
39
|
return (
|
|
45
|
-
<SessionRecorderContext.Provider value={{ instance: sessionRecorder }}>
|
|
40
|
+
<SessionRecorderContext.Provider value={{ instance: sessionRecorder, sessionState, isInitialized }}>
|
|
46
41
|
<ScreenRecorderView>{children}</ScreenRecorderView>
|
|
47
|
-
<
|
|
48
|
-
<View
|
|
49
|
-
style={{
|
|
50
|
-
position: 'absolute',
|
|
51
|
-
right: 0,
|
|
52
|
-
bottom: 100,
|
|
53
|
-
width: 48,
|
|
54
|
-
height: 48,
|
|
55
|
-
paddingTop: 16,
|
|
56
|
-
paddingLeft: 10,
|
|
57
|
-
backgroundColor: 'red',
|
|
58
|
-
borderTopLeftRadius: 24,
|
|
59
|
-
borderBottomLeftRadius: 24
|
|
60
|
-
}}
|
|
61
|
-
>
|
|
62
|
-
<Text style={{ color: 'white' }}>{sessionState === SessionState.started ? 'Stop' : 'Start'}</Text>
|
|
63
|
-
</View>
|
|
64
|
-
</Pressable>
|
|
42
|
+
{isInitialized && !!sessionRecorder.config.showWidget && <SessionRecorderWidget />}
|
|
65
43
|
</SessionRecorderContext.Provider>
|
|
66
44
|
)
|
|
67
45
|
}
|
package/src/index.ts
CHANGED
|
@@ -3,15 +3,7 @@ import SessionRecorder from './session-recorder'
|
|
|
3
3
|
export * from '@multiplayer-app/session-recorder-common'
|
|
4
4
|
export * from './context/SessionRecorderContext'
|
|
5
5
|
|
|
6
|
-
// Export
|
|
7
|
-
export {
|
|
8
|
-
detectPlatform,
|
|
9
|
-
isExpoEnvironment,
|
|
10
|
-
configureAppMetadata,
|
|
11
|
-
getPlatformAttributes,
|
|
12
|
-
getConfiguredAppMetadata,
|
|
13
|
-
} from './utils/platform'
|
|
14
|
-
|
|
6
|
+
// Export the class for type checking
|
|
15
7
|
export { SessionRecorder }
|
|
16
8
|
// Export the instance as default
|
|
17
9
|
export default SessionRecorder
|
package/src/patch/xhr.ts
CHANGED
|
@@ -17,10 +17,10 @@ export const setMaxCapturingHttpPayloadSize = (_maxCapturingHttpPayloadSize: num
|
|
|
17
17
|
maxCapturingHttpPayloadSize = _maxCapturingHttpPayloadSize
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
export const setShouldRecordHttpData = (
|
|
20
|
+
export const setShouldRecordHttpData = (shouldRecordBody: boolean, shouldRecordHeaders: boolean) => {
|
|
21
21
|
recordRequestHeaders = shouldRecordHeaders
|
|
22
22
|
recordResponseHeaders = shouldRecordHeaders
|
|
23
|
-
shouldRecordBody =
|
|
23
|
+
shouldRecordBody = shouldRecordBody
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
function _tryReadXHRBody({
|
|
@@ -30,6 +30,7 @@ function _tryReadXHRBody({
|
|
|
30
30
|
body: any | null | undefined
|
|
31
31
|
url: string | URL | RequestInfo
|
|
32
32
|
}): string | null {
|
|
33
|
+
|
|
33
34
|
if (isNullish(body)) {
|
|
34
35
|
return null
|
|
35
36
|
}
|
|
@@ -38,14 +39,13 @@ function _tryReadXHRBody({
|
|
|
38
39
|
return body
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
|
|
42
42
|
if (isFormData(body)) {
|
|
43
43
|
return formDataToQuery(body)
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
if (isObject(body)) {
|
|
47
47
|
try {
|
|
48
|
-
return JSON.stringify(body)
|
|
48
|
+
return JSON.stringify({ ...body })
|
|
49
49
|
} catch {
|
|
50
50
|
return '[XHR] Failed to stringify response object'
|
|
51
51
|
}
|
|
@@ -104,11 +104,11 @@ function _tryReadXHRBody({
|
|
|
104
104
|
return
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
|
|
108
107
|
// @ts-ignore
|
|
109
108
|
const responseHeaders: Record<string, string> = {}
|
|
110
|
-
const rawHeaders = xhr.getAllResponseHeaders()
|
|
111
|
-
const headers = rawHeaders.trim().split(/[\r\n]+/)
|
|
109
|
+
const rawHeaders = xhr.getAllResponseHeaders() || ''
|
|
110
|
+
const headers = rawHeaders.trim().split(/[\r\n]+/).filter(Boolean)
|
|
111
|
+
|
|
112
112
|
headers.forEach((line) => {
|
|
113
113
|
const parts = line.split(': ')
|
|
114
114
|
const header = parts.shift()
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import AsyncStorage from '@react-native-async-storage/async-storage'
|
|
2
2
|
import { SessionType } from '@multiplayer-app/session-recorder-common'
|
|
3
3
|
import { ISession, SessionState } from '../types'
|
|
4
|
+
import { logger } from '../utils'
|
|
4
5
|
|
|
5
6
|
interface CacheData {
|
|
6
7
|
sessionId: string | null
|
|
7
8
|
sessionType: SessionType | null
|
|
8
9
|
sessionState: SessionState | null
|
|
9
10
|
sessionObject: ISession | null
|
|
11
|
+
floatingButtonPosition: { x: number; y: number } | null
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
export class StorageService {
|
|
@@ -14,29 +16,42 @@ export class StorageService {
|
|
|
14
16
|
private static readonly SESSION_TYPE_KEY = 'session_type'
|
|
15
17
|
private static readonly SESSION_STATE_KEY = 'session_state'
|
|
16
18
|
private static readonly SESSION_OBJECT_KEY = 'session_object'
|
|
19
|
+
private static readonly FLOATING_BUTTON_POSITION_KEY = 'floating_button_position'
|
|
17
20
|
|
|
18
21
|
private static cache: CacheData = {
|
|
19
22
|
sessionId: null,
|
|
20
23
|
sessionType: null,
|
|
21
24
|
sessionState: null,
|
|
22
25
|
sessionObject: null,
|
|
26
|
+
floatingButtonPosition: null,
|
|
23
27
|
}
|
|
24
28
|
|
|
25
29
|
private static cacheInitialized = false
|
|
30
|
+
private static instance: StorageService | null = null
|
|
31
|
+
private static positionSaveTimeout: NodeJS.Timeout | null = null
|
|
26
32
|
|
|
27
|
-
constructor() {
|
|
28
|
-
|
|
33
|
+
private constructor() {
|
|
34
|
+
// Private constructor for singleton
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
static getInstance(): StorageService {
|
|
38
|
+
if (!StorageService.instance) {
|
|
39
|
+
StorageService.instance = new StorageService()
|
|
40
|
+
StorageService.initialize()
|
|
41
|
+
}
|
|
42
|
+
return StorageService.instance
|
|
29
43
|
}
|
|
30
44
|
|
|
31
45
|
private static async initializeCache(): Promise<void> {
|
|
32
46
|
if (StorageService.cacheInitialized) return
|
|
33
47
|
|
|
34
48
|
try {
|
|
35
|
-
const [sessionId, sessionType, sessionState, sessionObject] = await Promise.all([
|
|
49
|
+
const [sessionId, sessionType, sessionState, sessionObject, floatingButtonPosition] = await Promise.all([
|
|
36
50
|
AsyncStorage.getItem(StorageService.SESSION_ID_KEY),
|
|
37
51
|
AsyncStorage.getItem(StorageService.SESSION_TYPE_KEY),
|
|
38
52
|
AsyncStorage.getItem(StorageService.SESSION_STATE_KEY),
|
|
39
53
|
AsyncStorage.getItem(StorageService.SESSION_OBJECT_KEY),
|
|
54
|
+
AsyncStorage.getItem(StorageService.FLOATING_BUTTON_POSITION_KEY),
|
|
40
55
|
])
|
|
41
56
|
|
|
42
57
|
StorageService.cache = {
|
|
@@ -44,6 +59,7 @@ export class StorageService {
|
|
|
44
59
|
sessionType: sessionType as SessionType | null,
|
|
45
60
|
sessionState: sessionState as SessionState | null,
|
|
46
61
|
sessionObject: sessionObject ? JSON.parse(sessionObject) : null,
|
|
62
|
+
floatingButtonPosition: floatingButtonPosition ? JSON.parse(floatingButtonPosition) : null,
|
|
47
63
|
}
|
|
48
64
|
StorageService.cacheInitialized = true
|
|
49
65
|
} catch (error) {
|
|
@@ -121,6 +137,7 @@ export class StorageService {
|
|
|
121
137
|
try {
|
|
122
138
|
// Clear cache immediately
|
|
123
139
|
StorageService.cache = {
|
|
140
|
+
...StorageService.cache,
|
|
124
141
|
sessionId: null,
|
|
125
142
|
sessionType: null,
|
|
126
143
|
sessionState: null,
|
|
@@ -142,7 +159,7 @@ export class StorageService {
|
|
|
142
159
|
}
|
|
143
160
|
}
|
|
144
161
|
|
|
145
|
-
getAllSessionData(): CacheData {
|
|
162
|
+
getAllSessionData(): Omit<CacheData, 'floatingButtonPosition'> {
|
|
146
163
|
return {
|
|
147
164
|
sessionId: StorageService.cache.sessionId,
|
|
148
165
|
sessionType: StorageService.cache.sessionType,
|
|
@@ -151,6 +168,30 @@ export class StorageService {
|
|
|
151
168
|
}
|
|
152
169
|
}
|
|
153
170
|
|
|
171
|
+
saveFloatingButtonPosition(position: { x: number; y: number }): void {
|
|
172
|
+
try {
|
|
173
|
+
StorageService.cache.floatingButtonPosition = position
|
|
174
|
+
|
|
175
|
+
// Debounce AsyncStorage writes to avoid excessive I/O
|
|
176
|
+
if (StorageService.positionSaveTimeout) {
|
|
177
|
+
clearTimeout(StorageService.positionSaveTimeout)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
StorageService.positionSaveTimeout = setTimeout(() => {
|
|
181
|
+
AsyncStorage.setItem(StorageService.FLOATING_BUTTON_POSITION_KEY, JSON.stringify(position)).catch(error => {
|
|
182
|
+
logger.error('StorageService', 'Failed to persist floating button position', error)
|
|
183
|
+
})
|
|
184
|
+
}, 100) // 100ms debounce
|
|
185
|
+
} catch (error) {
|
|
186
|
+
logger.error('StorageService', 'Failed to save floating button position', error)
|
|
187
|
+
throw error
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
getFloatingButtonPosition(): { x: number; y: number } | null {
|
|
192
|
+
return StorageService.cache.floatingButtonPosition
|
|
193
|
+
}
|
|
194
|
+
|
|
154
195
|
// Initialize cache on first use - call this method when the service is first used
|
|
155
196
|
static async initialize(): Promise<void> {
|
|
156
197
|
await StorageService.initializeCache()
|
package/src/session-recorder.ts
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from './types'
|
|
17
17
|
import { getFormattedDate, isSessionActive, getNavigatorInfo } from './utils'
|
|
18
18
|
import { setMaxCapturingHttpPayloadSize, setShouldRecordHttpData } from './patch/xhr'
|
|
19
|
-
import { DEFAULT_MAX_HTTP_CAPTURING_PAYLOAD_SIZE, getSessionRecorderConfig } from './config'
|
|
19
|
+
import { BASE_CONFIG, DEFAULT_MAX_HTTP_CAPTURING_PAYLOAD_SIZE, getSessionRecorderConfig } from './config'
|
|
20
20
|
|
|
21
21
|
import { StorageService } from './services/storage.service'
|
|
22
22
|
import { ApiService, StartSessionRequest, StopSessionRequest } from './services/api.service'
|
|
@@ -30,11 +30,11 @@ type SessionRecorderEvents =
|
|
|
30
30
|
|
|
31
31
|
class SessionRecorder extends Observable<SessionRecorderEvents> implements ISessionRecorder, EventRecorder {
|
|
32
32
|
private _isInitialized = false
|
|
33
|
-
private _configs: SessionRecorderConfigs
|
|
33
|
+
private _configs: SessionRecorderConfigs
|
|
34
34
|
private _apiService = new ApiService()
|
|
35
35
|
private _tracer = new TracerReactNativeSDK()
|
|
36
36
|
private _recorder = new RecorderReactNativeSDK()
|
|
37
|
-
private _storageService =
|
|
37
|
+
private _storageService = StorageService.getInstance()
|
|
38
38
|
private _startRequestController: AbortController | null = null
|
|
39
39
|
|
|
40
40
|
// Session ID and state are stored in AsyncStorage
|
|
@@ -112,11 +112,16 @@ class SessionRecorder extends Observable<SessionRecorderEvents> implements ISess
|
|
|
112
112
|
return null
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
public get config(): SessionRecorderConfigs {
|
|
116
|
+
return this._configs
|
|
117
|
+
}
|
|
118
|
+
|
|
115
119
|
/**
|
|
116
120
|
* Initialize debugger with default or custom configurations
|
|
117
121
|
*/
|
|
118
122
|
constructor() {
|
|
119
123
|
super()
|
|
124
|
+
this._configs = BASE_CONFIG
|
|
120
125
|
// Initialize with stored session data if available
|
|
121
126
|
StorageService.initialize()
|
|
122
127
|
}
|
|
@@ -154,6 +159,7 @@ class SessionRecorder extends Observable<SessionRecorderEvents> implements ISess
|
|
|
154
159
|
this._isInitialized = true
|
|
155
160
|
this._checkOperation('init')
|
|
156
161
|
await this._loadStoredSessionData()
|
|
162
|
+
|
|
157
163
|
setMaxCapturingHttpPayloadSize(this._configs.maxCapturingHttpPayloadSize || DEFAULT_MAX_HTTP_CAPTURING_PAYLOAD_SIZE)
|
|
158
164
|
setShouldRecordHttpData(!this._configs.captureBody, this._configs.captureHeaders)
|
|
159
165
|
|