react-native-iinstall 0.1.0 → 0.2.0
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 +11 -1
- package/lib/FeedbackModal.d.ts +2 -0
- package/lib/FeedbackModal.js +166 -30
- package/lib/index.js +115 -14
- package/package.json +4 -2
- package/src/FeedbackModal.tsx +199 -42
- package/src/index.tsx +122 -15
package/README.md
CHANGED
|
@@ -54,10 +54,20 @@ export default App;
|
|
|
54
54
|
Add to `AndroidManifest.xml`:
|
|
55
55
|
```xml
|
|
56
56
|
<uses-permission android:name="android.permission.INTERNET" />
|
|
57
|
+
<!-- Required for Audio Feedback -->
|
|
58
|
+
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
|
59
|
+
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
|
60
|
+
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
|
57
61
|
```
|
|
58
62
|
|
|
59
63
|
### iOS
|
|
60
|
-
Add to `Info.plist
|
|
64
|
+
Add to `Info.plist`:
|
|
65
|
+
```xml
|
|
66
|
+
<key>NSMicrophoneUsageDescription</key>
|
|
67
|
+
<string>Allow access to microphone for audio feedback.</string>
|
|
68
|
+
<key>NSPhotoLibraryUsageDescription</key>
|
|
69
|
+
<string>Allow access to save screen recordings.</string>
|
|
70
|
+
```
|
|
61
71
|
|
|
62
72
|
## Troubleshooting
|
|
63
73
|
|
package/lib/FeedbackModal.d.ts
CHANGED
package/lib/FeedbackModal.js
CHANGED
|
@@ -40,12 +40,70 @@ exports.FeedbackModal = void 0;
|
|
|
40
40
|
const react_1 = __importStar(require("react"));
|
|
41
41
|
const react_native_1 = require("react-native");
|
|
42
42
|
const react_native_device_info_1 = __importDefault(require("react-native-device-info"));
|
|
43
|
-
const
|
|
43
|
+
const react_native_audio_recorder_player_1 = __importDefault(require("react-native-audio-recorder-player"));
|
|
44
|
+
const FeedbackModal = ({ visible, onClose, screenshotUri, videoUri, onStartRecording, apiKey, apiEndpoint }) => {
|
|
44
45
|
const [description, setDescription] = (0, react_1.useState)('');
|
|
45
46
|
const [isSending, setIsSending] = (0, react_1.useState)(false);
|
|
47
|
+
// Audio State
|
|
48
|
+
const [isRecordingAudio, setIsRecordingAudio] = (0, react_1.useState)(false);
|
|
49
|
+
const [audioUri, setAudioUri] = (0, react_1.useState)(null);
|
|
50
|
+
const [audioDuration, setAudioDuration] = (0, react_1.useState)('00:00');
|
|
51
|
+
const audioRecorderPlayer = (0, react_1.useRef)(new react_native_audio_recorder_player_1.default()).current;
|
|
52
|
+
(0, react_1.useEffect)(() => {
|
|
53
|
+
return () => {
|
|
54
|
+
// Cleanup
|
|
55
|
+
if (isRecordingAudio) {
|
|
56
|
+
audioRecorderPlayer.stopRecorder();
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}, [isRecordingAudio]);
|
|
60
|
+
const onStartAudioRecord = async () => {
|
|
61
|
+
if (react_native_1.Platform.OS === 'android') {
|
|
62
|
+
try {
|
|
63
|
+
const grants = await react_native_1.PermissionsAndroid.requestMultiple([
|
|
64
|
+
react_native_1.PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
|
|
65
|
+
react_native_1.PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE,
|
|
66
|
+
react_native_1.PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
|
|
67
|
+
]);
|
|
68
|
+
// Note: Android 13+ might need different permissions for media, but keeping it simple
|
|
69
|
+
if (grants['android.permission.RECORD_AUDIO'] !== react_native_1.PermissionsAndroid.RESULTS.GRANTED) {
|
|
70
|
+
react_native_1.Alert.alert('Permission needed', 'Audio recording permission is required.');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
console.warn(err);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const result = await audioRecorderPlayer.startRecorder();
|
|
81
|
+
audioRecorderPlayer.addRecordBackListener((e) => {
|
|
82
|
+
setAudioDuration(audioRecorderPlayer.mmssss(Math.floor(e.currentPosition)));
|
|
83
|
+
return;
|
|
84
|
+
});
|
|
85
|
+
setIsRecordingAudio(true);
|
|
86
|
+
setAudioUri(null);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
console.error('Failed to start recording', error);
|
|
90
|
+
react_native_1.Alert.alert('Error', 'Could not start audio recording');
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
const onStopAudioRecord = async () => {
|
|
94
|
+
try {
|
|
95
|
+
const result = await audioRecorderPlayer.stopRecorder();
|
|
96
|
+
audioRecorderPlayer.removeRecordBackListener();
|
|
97
|
+
setIsRecordingAudio(false);
|
|
98
|
+
setAudioUri(result);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
console.error('Failed to stop recording', error);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
46
104
|
const sendFeedback = async () => {
|
|
47
|
-
if (!description.trim()) {
|
|
48
|
-
react_native_1.Alert.alert('Please
|
|
105
|
+
if (!description.trim() && !audioUri && !videoUri) {
|
|
106
|
+
react_native_1.Alert.alert('Please provide a description, audio, or video.');
|
|
49
107
|
return;
|
|
50
108
|
}
|
|
51
109
|
setIsSending(true);
|
|
@@ -55,14 +113,30 @@ const FeedbackModal = ({ visible, onClose, screenshotUri, apiKey, apiEndpoint })
|
|
|
55
113
|
if (screenshotUri) {
|
|
56
114
|
const filename = screenshotUri.split('/').pop();
|
|
57
115
|
const match = /\.(\w+)$/.exec(filename || '');
|
|
58
|
-
const type = match ? `image/${match[1]}` : `image`;
|
|
116
|
+
const type = match ? `image/${match[1]}` : `image/png`;
|
|
59
117
|
formData.append('screenshot', {
|
|
60
118
|
uri: react_native_1.Platform.OS === 'ios' ? screenshotUri.replace('file://', '') : screenshotUri,
|
|
61
119
|
name: filename || 'screenshot.png',
|
|
62
120
|
type,
|
|
63
121
|
});
|
|
64
122
|
}
|
|
65
|
-
// 2.
|
|
123
|
+
// 2. Audio
|
|
124
|
+
if (audioUri) {
|
|
125
|
+
formData.append('audio', {
|
|
126
|
+
uri: react_native_1.Platform.OS === 'ios' ? audioUri.replace('file://', '') : audioUri,
|
|
127
|
+
name: 'feedback.m4a',
|
|
128
|
+
type: 'audio/m4a',
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
// 3. Video
|
|
132
|
+
if (videoUri) {
|
|
133
|
+
formData.append('video', {
|
|
134
|
+
uri: react_native_1.Platform.OS === 'ios' ? videoUri.replace('file://', '') : videoUri,
|
|
135
|
+
name: 'screen_record.mp4',
|
|
136
|
+
type: 'video/mp4',
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
// 4. Metadata
|
|
66
140
|
const metadata = {
|
|
67
141
|
device: react_native_device_info_1.default.getModel(),
|
|
68
142
|
systemName: react_native_device_info_1.default.getSystemName(),
|
|
@@ -73,11 +147,11 @@ const FeedbackModal = ({ visible, onClose, screenshotUri, apiKey, apiEndpoint })
|
|
|
73
147
|
isEmulator: await react_native_device_info_1.default.isEmulator(),
|
|
74
148
|
};
|
|
75
149
|
formData.append('metadata', JSON.stringify(metadata));
|
|
76
|
-
//
|
|
150
|
+
// 5. Other fields
|
|
77
151
|
formData.append('description', description);
|
|
78
152
|
formData.append('apiKey', apiKey);
|
|
79
|
-
formData.append('udid', await react_native_device_info_1.default.getUniqueId());
|
|
80
|
-
//
|
|
153
|
+
formData.append('udid', await react_native_device_info_1.default.getUniqueId());
|
|
154
|
+
// 6. Send
|
|
81
155
|
const response = await fetch(`${apiEndpoint}/api/feedback`, {
|
|
82
156
|
method: 'POST',
|
|
83
157
|
headers: {
|
|
@@ -89,6 +163,8 @@ const FeedbackModal = ({ visible, onClose, screenshotUri, apiKey, apiEndpoint })
|
|
|
89
163
|
if (response.ok) {
|
|
90
164
|
react_native_1.Alert.alert('Success', 'Feedback sent successfully!');
|
|
91
165
|
setDescription('');
|
|
166
|
+
setAudioUri(null);
|
|
167
|
+
setAudioDuration('00:00');
|
|
92
168
|
onClose();
|
|
93
169
|
}
|
|
94
170
|
else {
|
|
@@ -115,12 +191,39 @@ const FeedbackModal = ({ visible, onClose, screenshotUri, apiKey, apiEndpoint })
|
|
|
115
191
|
</react_native_1.TouchableOpacity>
|
|
116
192
|
</react_native_1.View>
|
|
117
193
|
|
|
118
|
-
{
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
194
|
+
{/* Media Previews */}
|
|
195
|
+
<react_native_1.View style={styles.mediaRow}>
|
|
196
|
+
{/* Screenshot Preview */}
|
|
197
|
+
{screenshotUri && !videoUri && (<react_native_1.View style={styles.mediaItem}>
|
|
198
|
+
<react_native_1.Image source={{ uri: screenshotUri }} style={styles.mediaPreview} resizeMode="cover"/>
|
|
199
|
+
<react_native_1.View style={styles.mediaBadge}><react_native_1.Text style={styles.mediaBadgeText}>Screenshot</react_native_1.Text></react_native_1.View>
|
|
200
|
+
</react_native_1.View>)}
|
|
201
|
+
|
|
202
|
+
{/* Video Preview (Placeholder) */}
|
|
203
|
+
{videoUri && (<react_native_1.View style={styles.mediaItem}>
|
|
204
|
+
<react_native_1.View style={[styles.mediaPreview, { backgroundColor: '#000', justifyContent: 'center', alignItems: 'center' }]}>
|
|
205
|
+
<react_native_1.Text style={{ color: '#FFF' }}>▶ Video</react_native_1.Text>
|
|
206
|
+
</react_native_1.View>
|
|
207
|
+
<react_native_1.View style={styles.mediaBadge}><react_native_1.Text style={styles.mediaBadgeText}>Screen Rec</react_native_1.Text></react_native_1.View>
|
|
208
|
+
</react_native_1.View>)}
|
|
209
|
+
</react_native_1.View>
|
|
210
|
+
|
|
211
|
+
<react_native_1.TextInput style={styles.input} placeholder="What happened? Describe the bug..." placeholderTextColor="#999" multiline value={description} onChangeText={setDescription}/>
|
|
212
|
+
|
|
213
|
+
{/* Media Actions Row */}
|
|
214
|
+
<react_native_1.View style={styles.actionsRow}>
|
|
215
|
+
{/* Audio Recorder */}
|
|
216
|
+
<react_native_1.TouchableOpacity style={[styles.actionBtn, isRecordingAudio ? styles.recordingBtn : undefined, audioUri ? styles.hasAudioBtn : undefined]} onPress={isRecordingAudio ? onStopAudioRecord : onStartAudioRecord}>
|
|
217
|
+
<react_native_1.Text style={[styles.actionBtnText, (isRecordingAudio || audioUri) ? { color: '#FFF' } : undefined]}>
|
|
218
|
+
{isRecordingAudio ? `Stop (${audioDuration})` : audioUri ? 'Re-record Audio' : '🎙 Record Audio'}
|
|
219
|
+
</react_native_1.Text>
|
|
220
|
+
</react_native_1.TouchableOpacity>
|
|
122
221
|
|
|
123
|
-
|
|
222
|
+
{/* Screen Recorder */}
|
|
223
|
+
{onStartRecording && !videoUri && (<react_native_1.TouchableOpacity style={styles.actionBtn} onPress={onStartRecording}>
|
|
224
|
+
<react_native_1.Text style={styles.actionBtnText}>🎥 Record Screen</react_native_1.Text>
|
|
225
|
+
</react_native_1.TouchableOpacity>)}
|
|
226
|
+
</react_native_1.View>
|
|
124
227
|
|
|
125
228
|
<react_native_1.TouchableOpacity style={[styles.submitBtn, isSending && styles.disabledBtn]} onPress={sendFeedback} disabled={isSending}>
|
|
126
229
|
{isSending ? (<react_native_1.ActivityIndicator color="#FFF"/>) : (<react_native_1.Text style={styles.submitText}>Send Feedback</react_native_1.Text>)}
|
|
@@ -141,7 +244,8 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
141
244
|
borderTopLeftRadius: 20,
|
|
142
245
|
borderTopRightRadius: 20,
|
|
143
246
|
padding: 20,
|
|
144
|
-
|
|
247
|
+
paddingBottom: 40,
|
|
248
|
+
maxHeight: '90%',
|
|
145
249
|
},
|
|
146
250
|
header: {
|
|
147
251
|
flexDirection: 'row',
|
|
@@ -159,47 +263,79 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
159
263
|
color: '#999',
|
|
160
264
|
padding: 5,
|
|
161
265
|
},
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
backgroundColor: '#f0f0f0',
|
|
165
|
-
borderRadius: 10,
|
|
266
|
+
mediaRow: {
|
|
267
|
+
flexDirection: 'row',
|
|
166
268
|
marginBottom: 15,
|
|
167
|
-
|
|
168
|
-
|
|
269
|
+
},
|
|
270
|
+
mediaItem: {
|
|
271
|
+
width: 100,
|
|
272
|
+
height: 100,
|
|
273
|
+
borderRadius: 10,
|
|
169
274
|
overflow: 'hidden',
|
|
275
|
+
marginRight: 10,
|
|
276
|
+
position: 'relative',
|
|
170
277
|
},
|
|
171
|
-
|
|
278
|
+
mediaPreview: {
|
|
172
279
|
width: '100%',
|
|
173
280
|
height: '100%',
|
|
174
281
|
},
|
|
175
|
-
|
|
282
|
+
mediaBadge: {
|
|
176
283
|
position: 'absolute',
|
|
177
|
-
bottom:
|
|
178
|
-
|
|
284
|
+
bottom: 0,
|
|
285
|
+
left: 0,
|
|
286
|
+
right: 0,
|
|
179
287
|
backgroundColor: 'rgba(0,0,0,0.6)',
|
|
288
|
+
padding: 4,
|
|
289
|
+
alignItems: 'center',
|
|
290
|
+
},
|
|
291
|
+
mediaBadgeText: {
|
|
180
292
|
color: '#FFF',
|
|
181
293
|
fontSize: 10,
|
|
182
|
-
|
|
183
|
-
borderRadius: 4,
|
|
294
|
+
fontWeight: 'bold',
|
|
184
295
|
},
|
|
185
296
|
input: {
|
|
186
297
|
backgroundColor: '#f9f9f9',
|
|
187
298
|
borderRadius: 10,
|
|
188
299
|
padding: 15,
|
|
189
|
-
height:
|
|
300
|
+
height: 100,
|
|
190
301
|
textAlignVertical: 'top',
|
|
191
302
|
fontSize: 16,
|
|
192
303
|
color: '#000',
|
|
304
|
+
marginBottom: 15,
|
|
305
|
+
},
|
|
306
|
+
actionsRow: {
|
|
307
|
+
flexDirection: 'row',
|
|
308
|
+
justifyContent: 'space-between',
|
|
193
309
|
marginBottom: 20,
|
|
194
310
|
},
|
|
311
|
+
actionBtn: {
|
|
312
|
+
flex: 1,
|
|
313
|
+
backgroundColor: '#f0f0f0',
|
|
314
|
+
padding: 12,
|
|
315
|
+
borderRadius: 8,
|
|
316
|
+
alignItems: 'center',
|
|
317
|
+
marginHorizontal: 5,
|
|
318
|
+
},
|
|
319
|
+
recordingBtn: {
|
|
320
|
+
backgroundColor: '#ff4444',
|
|
321
|
+
},
|
|
322
|
+
hasAudioBtn: {
|
|
323
|
+
backgroundColor: '#4CAF50',
|
|
324
|
+
},
|
|
325
|
+
actionBtnText: {
|
|
326
|
+
fontSize: 14,
|
|
327
|
+
fontWeight: '600',
|
|
328
|
+
color: '#333',
|
|
329
|
+
},
|
|
195
330
|
submitBtn: {
|
|
196
|
-
backgroundColor: '#
|
|
197
|
-
padding: 15,
|
|
331
|
+
backgroundColor: '#000',
|
|
198
332
|
borderRadius: 12,
|
|
333
|
+
padding: 16,
|
|
199
334
|
alignItems: 'center',
|
|
335
|
+
marginTop: 10,
|
|
200
336
|
},
|
|
201
337
|
disabledBtn: {
|
|
202
|
-
|
|
338
|
+
opacity: 0.7,
|
|
203
339
|
},
|
|
204
340
|
submitText: {
|
|
205
341
|
color: '#FFF',
|
package/lib/index.js
CHANGED
|
@@ -32,6 +32,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
32
32
|
return result;
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
35
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
39
|
exports.IInstall = void 0;
|
|
37
40
|
const react_1 = __importStar(require("react"));
|
|
@@ -39,22 +42,23 @@ const react_native_1 = require("react-native");
|
|
|
39
42
|
const react_native_view_shot_1 = require("react-native-view-shot");
|
|
40
43
|
const ShakeDetector_1 = require("./ShakeDetector");
|
|
41
44
|
const FeedbackModal_1 = require("./FeedbackModal");
|
|
45
|
+
const react_native_record_screen_1 = __importDefault(require("react-native-record-screen"));
|
|
42
46
|
const IInstall = ({ apiKey, apiEndpoint = 'https://iinstall.app', children, enabled = true }) => {
|
|
43
47
|
const [modalVisible, setModalVisible] = (0, react_1.useState)(false);
|
|
44
48
|
const [screenshotUri, setScreenshotUri] = (0, react_1.useState)(null);
|
|
49
|
+
const [videoUri, setVideoUri] = (0, react_1.useState)(null);
|
|
50
|
+
const [isRecording, setIsRecording] = (0, react_1.useState)(false);
|
|
45
51
|
const shakeDetectorRef = (0, react_1.useRef)(null);
|
|
52
|
+
// Refs for stable access in shake callback
|
|
53
|
+
const isRecordingRef = (0, react_1.useRef)(isRecording);
|
|
54
|
+
const modalVisibleRef = (0, react_1.useRef)(modalVisible);
|
|
46
55
|
(0, react_1.useEffect)(() => {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
shakeDetectorRef.current.start();
|
|
51
|
-
return () => {
|
|
52
|
-
shakeDetectorRef.current?.stop();
|
|
53
|
-
};
|
|
54
|
-
}, [enabled]);
|
|
56
|
+
isRecordingRef.current = isRecording;
|
|
57
|
+
modalVisibleRef.current = modalVisible;
|
|
58
|
+
}, [isRecording, modalVisible]);
|
|
55
59
|
const handleShake = async () => {
|
|
56
|
-
if (
|
|
57
|
-
return;
|
|
60
|
+
if (modalVisibleRef.current || isRecordingRef.current)
|
|
61
|
+
return;
|
|
58
62
|
try {
|
|
59
63
|
// Capture screenshot
|
|
60
64
|
const uri = await (0, react_native_view_shot_1.captureScreen)({
|
|
@@ -66,15 +70,112 @@ const IInstall = ({ apiKey, apiEndpoint = 'https://iinstall.app', children, enab
|
|
|
66
70
|
}
|
|
67
71
|
catch (error) {
|
|
68
72
|
console.error('IInstall: Failed to capture screenshot', error);
|
|
69
|
-
// Still show modal without screenshot
|
|
70
73
|
setScreenshotUri(null);
|
|
71
74
|
setModalVisible(true);
|
|
72
75
|
}
|
|
73
76
|
};
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
+
// Keep handleShake stable for the detector
|
|
78
|
+
const handleShakeCallback = (0, react_1.useRef)(handleShake);
|
|
79
|
+
(0, react_1.useEffect)(() => {
|
|
80
|
+
handleShakeCallback.current = handleShake;
|
|
81
|
+
});
|
|
82
|
+
(0, react_1.useEffect)(() => {
|
|
83
|
+
if (!enabled)
|
|
84
|
+
return;
|
|
85
|
+
// Use a wrapper to call the current handleShake
|
|
86
|
+
shakeDetectorRef.current = new ShakeDetector_1.ShakeDetector(() => handleShakeCallback.current());
|
|
87
|
+
shakeDetectorRef.current.start();
|
|
88
|
+
return () => {
|
|
89
|
+
shakeDetectorRef.current?.stop();
|
|
90
|
+
};
|
|
91
|
+
}, [enabled]);
|
|
92
|
+
const handleStartRecording = async () => {
|
|
93
|
+
setModalVisible(false);
|
|
94
|
+
setIsRecording(true);
|
|
95
|
+
try {
|
|
96
|
+
await react_native_record_screen_1.default.startRecording({ mic: true }).catch((error) => console.error(error));
|
|
97
|
+
}
|
|
98
|
+
catch (e) {
|
|
99
|
+
console.error('Failed to start recording', e);
|
|
100
|
+
setIsRecording(false);
|
|
101
|
+
setModalVisible(true); // Re-open if failed
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const handleStopRecording = async () => {
|
|
105
|
+
try {
|
|
106
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
107
|
+
const res = await react_native_record_screen_1.default.stopRecording().catch((error) => console.error(error));
|
|
108
|
+
setIsRecording(false);
|
|
109
|
+
if (res) {
|
|
110
|
+
setVideoUri(res.result.outputURL);
|
|
111
|
+
setModalVisible(true);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
catch (e) {
|
|
115
|
+
console.error('Failed to stop recording', e);
|
|
116
|
+
setIsRecording(false);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
return (<react_native_1.View style={react_native_1.StyleSheet.absoluteFill} pointerEvents="box-none">
|
|
120
|
+
<react_native_1.View style={react_native_1.StyleSheet.absoluteFill}>
|
|
121
|
+
{children}
|
|
122
|
+
</react_native_1.View>
|
|
123
|
+
|
|
124
|
+
{isRecording && (<react_native_1.SafeAreaView style={styles.recordingOverlay} pointerEvents="box-none">
|
|
125
|
+
<react_native_1.View style={styles.recordingContainer}>
|
|
126
|
+
<react_native_1.View style={styles.recordingDot}/>
|
|
127
|
+
<react_native_1.Text style={styles.recordingText}>Recording Screen...</react_native_1.Text>
|
|
128
|
+
<react_native_1.TouchableOpacity onPress={handleStopRecording} style={styles.stopButton}>
|
|
129
|
+
<react_native_1.Text style={styles.stopButtonText}>Stop</react_native_1.Text>
|
|
130
|
+
</react_native_1.TouchableOpacity>
|
|
131
|
+
</react_native_1.View>
|
|
132
|
+
</react_native_1.SafeAreaView>)}
|
|
133
|
+
|
|
134
|
+
<FeedbackModal_1.FeedbackModal visible={modalVisible} onClose={() => {
|
|
135
|
+
setModalVisible(false);
|
|
136
|
+
setVideoUri(null); // Clear video on close
|
|
137
|
+
setScreenshotUri(null);
|
|
138
|
+
}} screenshotUri={screenshotUri} videoUri={videoUri} onStartRecording={handleStartRecording} apiKey={apiKey} apiEndpoint={apiEndpoint}/>
|
|
77
139
|
</react_native_1.View>);
|
|
78
140
|
};
|
|
79
141
|
exports.IInstall = IInstall;
|
|
142
|
+
const styles = react_native_1.StyleSheet.create({
|
|
143
|
+
recordingOverlay: {
|
|
144
|
+
position: 'absolute',
|
|
145
|
+
top: 50,
|
|
146
|
+
alignSelf: 'center',
|
|
147
|
+
zIndex: 9999,
|
|
148
|
+
},
|
|
149
|
+
recordingContainer: {
|
|
150
|
+
flexDirection: 'row',
|
|
151
|
+
alignItems: 'center',
|
|
152
|
+
backgroundColor: 'rgba(0,0,0,0.8)',
|
|
153
|
+
padding: 10,
|
|
154
|
+
borderRadius: 30,
|
|
155
|
+
paddingHorizontal: 20,
|
|
156
|
+
},
|
|
157
|
+
recordingDot: {
|
|
158
|
+
width: 10,
|
|
159
|
+
height: 10,
|
|
160
|
+
borderRadius: 5,
|
|
161
|
+
backgroundColor: '#ff4444',
|
|
162
|
+
marginRight: 10,
|
|
163
|
+
},
|
|
164
|
+
recordingText: {
|
|
165
|
+
color: '#FFF',
|
|
166
|
+
fontWeight: '600',
|
|
167
|
+
marginRight: 15,
|
|
168
|
+
},
|
|
169
|
+
stopButton: {
|
|
170
|
+
backgroundColor: '#FFF',
|
|
171
|
+
paddingVertical: 5,
|
|
172
|
+
paddingHorizontal: 15,
|
|
173
|
+
borderRadius: 15,
|
|
174
|
+
},
|
|
175
|
+
stopButtonText: {
|
|
176
|
+
color: '#000',
|
|
177
|
+
fontWeight: 'bold',
|
|
178
|
+
fontSize: 12,
|
|
179
|
+
}
|
|
180
|
+
});
|
|
80
181
|
exports.default = exports.IInstall;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-iinstall",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "IInstall SDK for React Native - Shake to Report",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "IInstall SDK for React Native - Shake to Report with Audio & Video",
|
|
5
5
|
"author": "TesterFlow Team",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
"react-native-sensors": "^7.3.0",
|
|
34
34
|
"react-native-view-shot": "^3.1.2",
|
|
35
35
|
"react-native-device-info": "^10.0.0",
|
|
36
|
+
"react-native-audio-recorder-player": "^3.6.4",
|
|
37
|
+
"react-native-record-screen": "*",
|
|
36
38
|
"rxjs": "^7.0.0"
|
|
37
39
|
},
|
|
38
40
|
"devDependencies": {
|
package/src/FeedbackModal.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react';
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
Modal,
|
|
4
4
|
View,
|
|
@@ -10,14 +10,18 @@ import {
|
|
|
10
10
|
Image,
|
|
11
11
|
Alert,
|
|
12
12
|
Platform,
|
|
13
|
-
KeyboardAvoidingView
|
|
13
|
+
KeyboardAvoidingView,
|
|
14
|
+
PermissionsAndroid
|
|
14
15
|
} from 'react-native';
|
|
15
16
|
import DeviceInfo from 'react-native-device-info';
|
|
17
|
+
import AudioRecorderPlayer from 'react-native-audio-recorder-player';
|
|
16
18
|
|
|
17
19
|
interface FeedbackModalProps {
|
|
18
20
|
visible: boolean;
|
|
19
21
|
onClose: () => void;
|
|
20
22
|
screenshotUri: string | null;
|
|
23
|
+
videoUri?: string | null;
|
|
24
|
+
onStartRecording?: () => void;
|
|
21
25
|
apiKey: string;
|
|
22
26
|
apiEndpoint: string;
|
|
23
27
|
}
|
|
@@ -26,15 +30,79 @@ export const FeedbackModal: React.FC<FeedbackModalProps> = ({
|
|
|
26
30
|
visible,
|
|
27
31
|
onClose,
|
|
28
32
|
screenshotUri,
|
|
33
|
+
videoUri,
|
|
34
|
+
onStartRecording,
|
|
29
35
|
apiKey,
|
|
30
36
|
apiEndpoint
|
|
31
37
|
}) => {
|
|
32
38
|
const [description, setDescription] = useState('');
|
|
33
39
|
const [isSending, setIsSending] = useState(false);
|
|
40
|
+
|
|
41
|
+
// Audio State
|
|
42
|
+
const [isRecordingAudio, setIsRecordingAudio] = useState(false);
|
|
43
|
+
const [audioUri, setAudioUri] = useState<string | null>(null);
|
|
44
|
+
const [audioDuration, setAudioDuration] = useState('00:00');
|
|
45
|
+
|
|
46
|
+
const audioRecorderPlayer = useRef(new AudioRecorderPlayer()).current;
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
return () => {
|
|
50
|
+
// Cleanup
|
|
51
|
+
if (isRecordingAudio) {
|
|
52
|
+
audioRecorderPlayer.stopRecorder();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}, [isRecordingAudio]);
|
|
56
|
+
|
|
57
|
+
const onStartAudioRecord = async () => {
|
|
58
|
+
if (Platform.OS === 'android') {
|
|
59
|
+
try {
|
|
60
|
+
const grants = await PermissionsAndroid.requestMultiple([
|
|
61
|
+
PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
|
|
62
|
+
PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE,
|
|
63
|
+
PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
|
|
64
|
+
]);
|
|
65
|
+
// Note: Android 13+ might need different permissions for media, but keeping it simple
|
|
66
|
+
if (
|
|
67
|
+
grants['android.permission.RECORD_AUDIO'] !== PermissionsAndroid.RESULTS.GRANTED
|
|
68
|
+
) {
|
|
69
|
+
Alert.alert('Permission needed', 'Audio recording permission is required.');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
} catch (err) {
|
|
73
|
+
console.warn(err);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const result = await audioRecorderPlayer.startRecorder();
|
|
80
|
+
audioRecorderPlayer.addRecordBackListener((e) => {
|
|
81
|
+
setAudioDuration(audioRecorderPlayer.mmssss(Math.floor(e.currentPosition)));
|
|
82
|
+
return;
|
|
83
|
+
});
|
|
84
|
+
setIsRecordingAudio(true);
|
|
85
|
+
setAudioUri(null);
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error('Failed to start recording', error);
|
|
88
|
+
Alert.alert('Error', 'Could not start audio recording');
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const onStopAudioRecord = async () => {
|
|
93
|
+
try {
|
|
94
|
+
const result = await audioRecorderPlayer.stopRecorder();
|
|
95
|
+
audioRecorderPlayer.removeRecordBackListener();
|
|
96
|
+
setIsRecordingAudio(false);
|
|
97
|
+
setAudioUri(result);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error('Failed to stop recording', error);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
34
102
|
|
|
35
103
|
const sendFeedback = async () => {
|
|
36
|
-
if (!description.trim()) {
|
|
37
|
-
Alert.alert('Please
|
|
104
|
+
if (!description.trim() && !audioUri && !videoUri) {
|
|
105
|
+
Alert.alert('Please provide a description, audio, or video.');
|
|
38
106
|
return;
|
|
39
107
|
}
|
|
40
108
|
|
|
@@ -47,7 +115,7 @@ export const FeedbackModal: React.FC<FeedbackModalProps> = ({
|
|
|
47
115
|
if (screenshotUri) {
|
|
48
116
|
const filename = screenshotUri.split('/').pop();
|
|
49
117
|
const match = /\.(\w+)$/.exec(filename || '');
|
|
50
|
-
const type = match ? `image/${match[1]}` : `image`;
|
|
118
|
+
const type = match ? `image/${match[1]}` : `image/png`;
|
|
51
119
|
|
|
52
120
|
formData.append('screenshot', {
|
|
53
121
|
uri: Platform.OS === 'ios' ? screenshotUri.replace('file://', '') : screenshotUri,
|
|
@@ -56,7 +124,25 @@ export const FeedbackModal: React.FC<FeedbackModalProps> = ({
|
|
|
56
124
|
} as any);
|
|
57
125
|
}
|
|
58
126
|
|
|
59
|
-
// 2.
|
|
127
|
+
// 2. Audio
|
|
128
|
+
if (audioUri) {
|
|
129
|
+
formData.append('audio', {
|
|
130
|
+
uri: Platform.OS === 'ios' ? audioUri.replace('file://', '') : audioUri,
|
|
131
|
+
name: 'feedback.m4a',
|
|
132
|
+
type: 'audio/m4a',
|
|
133
|
+
} as any);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 3. Video
|
|
137
|
+
if (videoUri) {
|
|
138
|
+
formData.append('video', {
|
|
139
|
+
uri: Platform.OS === 'ios' ? videoUri.replace('file://', '') : videoUri,
|
|
140
|
+
name: 'screen_record.mp4',
|
|
141
|
+
type: 'video/mp4',
|
|
142
|
+
} as any);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 4. Metadata
|
|
60
146
|
const metadata = {
|
|
61
147
|
device: DeviceInfo.getModel(),
|
|
62
148
|
systemName: DeviceInfo.getSystemName(),
|
|
@@ -68,12 +154,12 @@ export const FeedbackModal: React.FC<FeedbackModalProps> = ({
|
|
|
68
154
|
};
|
|
69
155
|
formData.append('metadata', JSON.stringify(metadata));
|
|
70
156
|
|
|
71
|
-
//
|
|
157
|
+
// 5. Other fields
|
|
72
158
|
formData.append('description', description);
|
|
73
159
|
formData.append('apiKey', apiKey);
|
|
74
|
-
formData.append('udid', await DeviceInfo.getUniqueId());
|
|
160
|
+
formData.append('udid', await DeviceInfo.getUniqueId());
|
|
75
161
|
|
|
76
|
-
//
|
|
162
|
+
// 6. Send
|
|
77
163
|
const response = await fetch(`${apiEndpoint}/api/feedback`, {
|
|
78
164
|
method: 'POST',
|
|
79
165
|
headers: {
|
|
@@ -87,6 +173,8 @@ export const FeedbackModal: React.FC<FeedbackModalProps> = ({
|
|
|
87
173
|
if (response.ok) {
|
|
88
174
|
Alert.alert('Success', 'Feedback sent successfully!');
|
|
89
175
|
setDescription('');
|
|
176
|
+
setAudioUri(null);
|
|
177
|
+
setAudioDuration('00:00');
|
|
90
178
|
onClose();
|
|
91
179
|
} else {
|
|
92
180
|
throw new Error(result.error || 'Failed to send');
|
|
@@ -116,12 +204,26 @@ export const FeedbackModal: React.FC<FeedbackModalProps> = ({
|
|
|
116
204
|
</TouchableOpacity>
|
|
117
205
|
</View>
|
|
118
206
|
|
|
119
|
-
{
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
207
|
+
{/* Media Previews */}
|
|
208
|
+
<View style={styles.mediaRow}>
|
|
209
|
+
{/* Screenshot Preview */}
|
|
210
|
+
{screenshotUri && !videoUri && (
|
|
211
|
+
<View style={styles.mediaItem}>
|
|
212
|
+
<Image source={{ uri: screenshotUri }} style={styles.mediaPreview} resizeMode="cover" />
|
|
213
|
+
<View style={styles.mediaBadge}><Text style={styles.mediaBadgeText}>Screenshot</Text></View>
|
|
214
|
+
</View>
|
|
215
|
+
)}
|
|
216
|
+
|
|
217
|
+
{/* Video Preview (Placeholder) */}
|
|
218
|
+
{videoUri && (
|
|
219
|
+
<View style={styles.mediaItem}>
|
|
220
|
+
<View style={[styles.mediaPreview, { backgroundColor: '#000', justifyContent: 'center', alignItems: 'center' }]}>
|
|
221
|
+
<Text style={{color: '#FFF'}}>▶ Video</Text>
|
|
222
|
+
</View>
|
|
223
|
+
<View style={styles.mediaBadge}><Text style={styles.mediaBadgeText}>Screen Rec</Text></View>
|
|
224
|
+
</View>
|
|
225
|
+
)}
|
|
226
|
+
</View>
|
|
125
227
|
|
|
126
228
|
<TextInput
|
|
127
229
|
style={styles.input}
|
|
@@ -130,9 +232,31 @@ export const FeedbackModal: React.FC<FeedbackModalProps> = ({
|
|
|
130
232
|
multiline
|
|
131
233
|
value={description}
|
|
132
234
|
onChangeText={setDescription}
|
|
133
|
-
autoFocus
|
|
134
235
|
/>
|
|
135
236
|
|
|
237
|
+
{/* Media Actions Row */}
|
|
238
|
+
<View style={styles.actionsRow}>
|
|
239
|
+
{/* Audio Recorder */}
|
|
240
|
+
<TouchableOpacity
|
|
241
|
+
style={[styles.actionBtn, isRecordingAudio ? styles.recordingBtn : undefined, audioUri ? styles.hasAudioBtn : undefined]}
|
|
242
|
+
onPress={isRecordingAudio ? onStopAudioRecord : onStartAudioRecord}
|
|
243
|
+
>
|
|
244
|
+
<Text style={[styles.actionBtnText, (isRecordingAudio || audioUri) ? { color: '#FFF' } : undefined]}>
|
|
245
|
+
{isRecordingAudio ? `Stop (${audioDuration})` : audioUri ? 'Re-record Audio' : '🎙 Record Audio'}
|
|
246
|
+
</Text>
|
|
247
|
+
</TouchableOpacity>
|
|
248
|
+
|
|
249
|
+
{/* Screen Recorder */}
|
|
250
|
+
{onStartRecording && !videoUri && (
|
|
251
|
+
<TouchableOpacity
|
|
252
|
+
style={styles.actionBtn}
|
|
253
|
+
onPress={onStartRecording}
|
|
254
|
+
>
|
|
255
|
+
<Text style={styles.actionBtnText}>🎥 Record Screen</Text>
|
|
256
|
+
</TouchableOpacity>
|
|
257
|
+
)}
|
|
258
|
+
</View>
|
|
259
|
+
|
|
136
260
|
<TouchableOpacity
|
|
137
261
|
style={[styles.submitBtn, isSending && styles.disabledBtn]}
|
|
138
262
|
onPress={sendFeedback}
|
|
@@ -161,7 +285,8 @@ const styles = StyleSheet.create({
|
|
|
161
285
|
borderTopLeftRadius: 20,
|
|
162
286
|
borderTopRightRadius: 20,
|
|
163
287
|
padding: 20,
|
|
164
|
-
|
|
288
|
+
paddingBottom: 40,
|
|
289
|
+
maxHeight: '90%',
|
|
165
290
|
},
|
|
166
291
|
header: {
|
|
167
292
|
flexDirection: 'row',
|
|
@@ -179,47 +304,79 @@ const styles = StyleSheet.create({
|
|
|
179
304
|
color: '#999',
|
|
180
305
|
padding: 5,
|
|
181
306
|
},
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
borderRadius: 10,
|
|
186
|
-
marginBottom: 15,
|
|
187
|
-
justifyContent: 'center',
|
|
188
|
-
alignItems: 'center',
|
|
189
|
-
overflow: 'hidden',
|
|
307
|
+
mediaRow: {
|
|
308
|
+
flexDirection: 'row',
|
|
309
|
+
marginBottom: 15,
|
|
190
310
|
},
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
311
|
+
mediaItem: {
|
|
312
|
+
width: 100,
|
|
313
|
+
height: 100,
|
|
314
|
+
borderRadius: 10,
|
|
315
|
+
overflow: 'hidden',
|
|
316
|
+
marginRight: 10,
|
|
317
|
+
position: 'relative',
|
|
194
318
|
},
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
319
|
+
mediaPreview: {
|
|
320
|
+
width: '100%',
|
|
321
|
+
height: '100%',
|
|
322
|
+
},
|
|
323
|
+
mediaBadge: {
|
|
324
|
+
position: 'absolute',
|
|
325
|
+
bottom: 0,
|
|
326
|
+
left: 0,
|
|
327
|
+
right: 0,
|
|
328
|
+
backgroundColor: 'rgba(0,0,0,0.6)',
|
|
329
|
+
padding: 4,
|
|
330
|
+
alignItems: 'center',
|
|
331
|
+
},
|
|
332
|
+
mediaBadgeText: {
|
|
333
|
+
color: '#FFF',
|
|
334
|
+
fontSize: 10,
|
|
335
|
+
fontWeight: 'bold',
|
|
204
336
|
},
|
|
205
337
|
input: {
|
|
206
338
|
backgroundColor: '#f9f9f9',
|
|
207
339
|
borderRadius: 10,
|
|
208
340
|
padding: 15,
|
|
209
|
-
height:
|
|
341
|
+
height: 100,
|
|
210
342
|
textAlignVertical: 'top',
|
|
211
343
|
fontSize: 16,
|
|
212
344
|
color: '#000',
|
|
213
|
-
marginBottom:
|
|
345
|
+
marginBottom: 15,
|
|
346
|
+
},
|
|
347
|
+
actionsRow: {
|
|
348
|
+
flexDirection: 'row',
|
|
349
|
+
justifyContent: 'space-between',
|
|
350
|
+
marginBottom: 20,
|
|
351
|
+
},
|
|
352
|
+
actionBtn: {
|
|
353
|
+
flex: 1,
|
|
354
|
+
backgroundColor: '#f0f0f0',
|
|
355
|
+
padding: 12,
|
|
356
|
+
borderRadius: 8,
|
|
357
|
+
alignItems: 'center',
|
|
358
|
+
marginHorizontal: 5,
|
|
359
|
+
},
|
|
360
|
+
recordingBtn: {
|
|
361
|
+
backgroundColor: '#ff4444',
|
|
362
|
+
},
|
|
363
|
+
hasAudioBtn: {
|
|
364
|
+
backgroundColor: '#4CAF50',
|
|
365
|
+
},
|
|
366
|
+
actionBtnText: {
|
|
367
|
+
fontSize: 14,
|
|
368
|
+
fontWeight: '600',
|
|
369
|
+
color: '#333',
|
|
214
370
|
},
|
|
215
371
|
submitBtn: {
|
|
216
|
-
backgroundColor: '#
|
|
217
|
-
padding: 15,
|
|
372
|
+
backgroundColor: '#000',
|
|
218
373
|
borderRadius: 12,
|
|
374
|
+
padding: 16,
|
|
219
375
|
alignItems: 'center',
|
|
376
|
+
marginTop: 10,
|
|
220
377
|
},
|
|
221
378
|
disabledBtn: {
|
|
222
|
-
|
|
379
|
+
opacity: 0.7,
|
|
223
380
|
},
|
|
224
381
|
submitText: {
|
|
225
382
|
color: '#FFF',
|
package/src/index.tsx
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import React, { useEffect, useState, useRef } from 'react';
|
|
2
|
-
import { View, StyleSheet } from 'react-native';
|
|
2
|
+
import { View, StyleSheet, TouchableOpacity, Text, SafeAreaView, Platform } from 'react-native';
|
|
3
3
|
import { captureScreen } from 'react-native-view-shot';
|
|
4
4
|
import { ShakeDetector } from './ShakeDetector';
|
|
5
5
|
import { FeedbackModal } from './FeedbackModal';
|
|
6
|
+
import RecordScreen from 'react-native-record-screen';
|
|
6
7
|
|
|
7
8
|
interface IInstallProps {
|
|
8
9
|
apiKey: string;
|
|
@@ -19,21 +20,22 @@ export const IInstall: React.FC<IInstallProps> = ({
|
|
|
19
20
|
}) => {
|
|
20
21
|
const [modalVisible, setModalVisible] = useState(false);
|
|
21
22
|
const [screenshotUri, setScreenshotUri] = useState<string | null>(null);
|
|
23
|
+
const [videoUri, setVideoUri] = useState<string | null>(null);
|
|
24
|
+
const [isRecording, setIsRecording] = useState(false);
|
|
25
|
+
|
|
22
26
|
const shakeDetectorRef = useRef<ShakeDetector | null>(null);
|
|
27
|
+
|
|
28
|
+
// Refs for stable access in shake callback
|
|
29
|
+
const isRecordingRef = useRef(isRecording);
|
|
30
|
+
const modalVisibleRef = useRef(modalVisible);
|
|
23
31
|
|
|
24
32
|
useEffect(() => {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
shakeDetectorRef.current.start();
|
|
29
|
-
|
|
30
|
-
return () => {
|
|
31
|
-
shakeDetectorRef.current?.stop();
|
|
32
|
-
};
|
|
33
|
-
}, [enabled]);
|
|
33
|
+
isRecordingRef.current = isRecording;
|
|
34
|
+
modalVisibleRef.current = modalVisible;
|
|
35
|
+
}, [isRecording, modalVisible]);
|
|
34
36
|
|
|
35
37
|
const handleShake = async () => {
|
|
36
|
-
if (
|
|
38
|
+
if (modalVisibleRef.current || isRecordingRef.current) return;
|
|
37
39
|
|
|
38
40
|
try {
|
|
39
41
|
// Capture screenshot
|
|
@@ -45,19 +47,84 @@ export const IInstall: React.FC<IInstallProps> = ({
|
|
|
45
47
|
setModalVisible(true);
|
|
46
48
|
} catch (error) {
|
|
47
49
|
console.error('IInstall: Failed to capture screenshot', error);
|
|
48
|
-
// Still show modal without screenshot
|
|
49
50
|
setScreenshotUri(null);
|
|
50
51
|
setModalVisible(true);
|
|
51
52
|
}
|
|
52
53
|
};
|
|
54
|
+
|
|
55
|
+
// Keep handleShake stable for the detector
|
|
56
|
+
const handleShakeCallback = useRef(handleShake);
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
handleShakeCallback.current = handleShake;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (!enabled) return;
|
|
63
|
+
|
|
64
|
+
// Use a wrapper to call the current handleShake
|
|
65
|
+
shakeDetectorRef.current = new ShakeDetector(() => handleShakeCallback.current());
|
|
66
|
+
shakeDetectorRef.current.start();
|
|
67
|
+
|
|
68
|
+
return () => {
|
|
69
|
+
shakeDetectorRef.current?.stop();
|
|
70
|
+
};
|
|
71
|
+
}, [enabled]);
|
|
72
|
+
|
|
73
|
+
const handleStartRecording = async () => {
|
|
74
|
+
setModalVisible(false);
|
|
75
|
+
setIsRecording(true);
|
|
76
|
+
try {
|
|
77
|
+
await RecordScreen.startRecording({ mic: true }).catch((error: any) => console.error(error));
|
|
78
|
+
} catch (e) {
|
|
79
|
+
console.error('Failed to start recording', e);
|
|
80
|
+
setIsRecording(false);
|
|
81
|
+
setModalVisible(true); // Re-open if failed
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const handleStopRecording = async () => {
|
|
86
|
+
try {
|
|
87
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
88
|
+
const res: any = await RecordScreen.stopRecording().catch((error: any) => console.error(error));
|
|
89
|
+
setIsRecording(false);
|
|
90
|
+
if (res) {
|
|
91
|
+
setVideoUri(res.result.outputURL);
|
|
92
|
+
setModalVisible(true);
|
|
93
|
+
}
|
|
94
|
+
} catch (e) {
|
|
95
|
+
console.error('Failed to stop recording', e);
|
|
96
|
+
setIsRecording(false);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
53
99
|
|
|
54
100
|
return (
|
|
55
|
-
<View style={StyleSheet.absoluteFill}>
|
|
56
|
-
{
|
|
101
|
+
<View style={StyleSheet.absoluteFill} pointerEvents="box-none">
|
|
102
|
+
<View style={StyleSheet.absoluteFill}>
|
|
103
|
+
{children}
|
|
104
|
+
</View>
|
|
105
|
+
|
|
106
|
+
{isRecording && (
|
|
107
|
+
<SafeAreaView style={styles.recordingOverlay} pointerEvents="box-none">
|
|
108
|
+
<View style={styles.recordingContainer}>
|
|
109
|
+
<View style={styles.recordingDot} />
|
|
110
|
+
<Text style={styles.recordingText}>Recording Screen...</Text>
|
|
111
|
+
<TouchableOpacity onPress={handleStopRecording} style={styles.stopButton}>
|
|
112
|
+
<Text style={styles.stopButtonText}>Stop</Text>
|
|
113
|
+
</TouchableOpacity>
|
|
114
|
+
</View>
|
|
115
|
+
</SafeAreaView>
|
|
116
|
+
)}
|
|
117
|
+
|
|
57
118
|
<FeedbackModal
|
|
58
119
|
visible={modalVisible}
|
|
59
|
-
onClose={() =>
|
|
120
|
+
onClose={() => {
|
|
121
|
+
setModalVisible(false);
|
|
122
|
+
setVideoUri(null); // Clear video on close
|
|
123
|
+
setScreenshotUri(null);
|
|
124
|
+
}}
|
|
60
125
|
screenshotUri={screenshotUri}
|
|
126
|
+
videoUri={videoUri}
|
|
127
|
+
onStartRecording={handleStartRecording}
|
|
61
128
|
apiKey={apiKey}
|
|
62
129
|
apiEndpoint={apiEndpoint}
|
|
63
130
|
/>
|
|
@@ -65,4 +132,44 @@ export const IInstall: React.FC<IInstallProps> = ({
|
|
|
65
132
|
);
|
|
66
133
|
};
|
|
67
134
|
|
|
135
|
+
const styles = StyleSheet.create({
|
|
136
|
+
recordingOverlay: {
|
|
137
|
+
position: 'absolute',
|
|
138
|
+
top: 50,
|
|
139
|
+
alignSelf: 'center',
|
|
140
|
+
zIndex: 9999,
|
|
141
|
+
},
|
|
142
|
+
recordingContainer: {
|
|
143
|
+
flexDirection: 'row',
|
|
144
|
+
alignItems: 'center',
|
|
145
|
+
backgroundColor: 'rgba(0,0,0,0.8)',
|
|
146
|
+
padding: 10,
|
|
147
|
+
borderRadius: 30,
|
|
148
|
+
paddingHorizontal: 20,
|
|
149
|
+
},
|
|
150
|
+
recordingDot: {
|
|
151
|
+
width: 10,
|
|
152
|
+
height: 10,
|
|
153
|
+
borderRadius: 5,
|
|
154
|
+
backgroundColor: '#ff4444',
|
|
155
|
+
marginRight: 10,
|
|
156
|
+
},
|
|
157
|
+
recordingText: {
|
|
158
|
+
color: '#FFF',
|
|
159
|
+
fontWeight: '600',
|
|
160
|
+
marginRight: 15,
|
|
161
|
+
},
|
|
162
|
+
stopButton: {
|
|
163
|
+
backgroundColor: '#FFF',
|
|
164
|
+
paddingVertical: 5,
|
|
165
|
+
paddingHorizontal: 15,
|
|
166
|
+
borderRadius: 15,
|
|
167
|
+
},
|
|
168
|
+
stopButtonText: {
|
|
169
|
+
color: '#000',
|
|
170
|
+
fontWeight: 'bold',
|
|
171
|
+
fontSize: 12,
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
68
175
|
export default IInstall;
|