react-native-iinstall 0.2.3 → 0.2.7

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.
@@ -0,0 +1,187 @@
1
+ # IInstall Integration Guide
2
+
3
+ This guide explains how to integrate the IInstall SDK into your React Native application and clarifies the workflow for API keys and installer generation.
4
+
5
+ ## 🚀 The Integration Workflow (Read This First)
6
+
7
+ There is a common "Chicken and Egg" confusion about when the API Key is created. Here is the correct order of operations:
8
+
9
+ 1. **Create Project (Dashboard)**: You create a project in the IInstall Dashboard *first*.
10
+ * **Result**: The Dashboard generates a unique **API Key** for your project immediately.
11
+ 2. **Integrate SDK (Code)**: You copy this API Key and add it to your React Native app's source code.
12
+ 3. **Generate Installer (Build)**: You build your app (APK/IPA). The API Key is now "baked into" the app.
13
+ 4. **Upload Build**: You upload the generated installer file to the Dashboard.
14
+
15
+ **Key Takeaway**: The API Key exists **BEFORE** you generate the installer. You do not need to upload a build to get a key.
16
+
17
+ ---
18
+
19
+ ## 1. Requirements & Compatibility
20
+
21
+ Before starting, ensure your project meets these minimum requirements:
22
+ - **React Native**: Version 0.60.0 or higher (for auto-linking support).
23
+ - **iOS**: Version 11.0 or higher.
24
+ - **Android**: Version 5.0 (API Level 21) or higher.
25
+
26
+ ---
27
+
28
+ ## 2. Get Your API Key
29
+
30
+ 1. Log in to your IInstall Dashboard.
31
+ 2. Click **"+ New Project"** (or use the empty state button).
32
+ 3. Enter your App Name and Platform (iOS/Android).
33
+ 4. Once created, you will be redirected to the **Project Dashboard**.
34
+ 5. Look for the **"API Integration"** card (usually on the right side or bottom of the stats grid).
35
+ 6. Copy the **Project API Key** (it looks like a long string of random characters).
36
+
37
+ ---
38
+
39
+ ## 3. Install the SDK in React Native
40
+
41
+ In your React Native project directory, run:
42
+
43
+ ```bash
44
+ npm install react-native-iinstall
45
+ # OR
46
+ yarn add react-native-iinstall
47
+ ```
48
+
49
+ **Note**: The SDK automatically includes necessary dependencies like `react-native-sensors` and `react-native-view-shot`.
50
+
51
+ ### Updating to Latest Version (v0.2.7)
52
+
53
+ If you're using an older version, update to get the latest audio/video improvements:
54
+
55
+ ```bash
56
+ npm install react-native-iinstall@0.2.7
57
+ # OR
58
+ yarn add react-native-iinstall@0.2.7
59
+ ```
60
+
61
+ **Key improvements in v0.2.7:**
62
+ - Fixed audio codec compatibility (AAC instead of ALAC)
63
+ - Enhanced screen recording reliability
64
+ - Improved modal state management
65
+
66
+ **iOS Specific Step:**
67
+ If you are on iOS and not using Expo Go, remember to install pods:
68
+ ```bash
69
+ cd ios && pod install
70
+ ```
71
+
72
+ ---
73
+
74
+ ## 4. Configure the SDK
75
+
76
+ Open your root component file (usually `App.tsx` or `App.js`). Wrap your main application with the `<IInstall>` provider.
77
+
78
+ ```tsx
79
+ import React from 'react';
80
+ import { IInstall } from 'react-native-iinstall';
81
+ import AppNavigation from './src/AppNavigation'; // Your main app component
82
+
83
+ const App = () => {
84
+ return (
85
+ // PASTE YOUR API KEY HERE
86
+ <IInstall
87
+ apiKey="YOUR_COPIED_API_KEY_FROM_STEP_1"
88
+ apiEndpoint="https://iinstall.app"
89
+ enabled={true} // Set to false in production if desired
90
+ >
91
+ <AppNavigation />
92
+ </IInstall>
93
+ );
94
+ };
95
+
96
+ export default App;
97
+ ```
98
+
99
+ ---
100
+
101
+ ## 5. Audio & Video Feedback (v0.2.7+)
102
+
103
+ The SDK now supports **Audio Feedback** (Voice Notes) and **Screen Recording** with enhanced compatibility and reliability.
104
+
105
+ ### Key Improvements in v0.2.7
106
+
107
+ 1. **Audio Codec Fix**: Audio recordings now use AAC codec instead of ALAC, ensuring compatibility with dashboard audio players
108
+ 2. **Modal State Management**: Improved handling of audio state to prevent stale data between feedback sessions
109
+ 3. **Screen Recording URI Extraction**: Enhanced reliability for capturing screen recordings across different devices
110
+
111
+ ### Installation
112
+
113
+ These features require native modules. If you haven't already, run:
114
+
115
+ ```bash
116
+ cd ios && pod install
117
+ ```
118
+
119
+ ### Permissions Required
120
+
121
+ To enable these features, you must add the following permissions to your app configuration files:
122
+
123
+ **Android (`android/app/src/main/AndroidManifest.xml`):**
124
+
125
+ Add these lines inside the `<manifest>` tag:
126
+
127
+ ```xml
128
+ <uses-permission android:name="android.permission.RECORD_AUDIO" />
129
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
130
+ <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
131
+ ```
132
+
133
+ **iOS (`ios/YourApp/Info.plist`):**
134
+
135
+ Add these keys inside the `<dict>` tag:
136
+
137
+ ```xml
138
+ <key>NSMicrophoneUsageDescription</key>
139
+ <string>Allow access to microphone for audio feedback.</string>
140
+ <key>NSPhotoLibraryUsageDescription</key>
141
+ <string>Allow access to save screen recordings.</string>
142
+ ```
143
+
144
+ ### Audio Quality Notes
145
+
146
+ - Audio files are encoded as AAC (Advanced Audio Coding) for maximum compatibility
147
+ - Files are saved as `.m4a` format
148
+ - Dashboard audio player will now correctly display duration and play audio feedback
149
+
150
+ ### Screen Recording Reliability
151
+
152
+ - Enhanced URI extraction handles various device-specific response formats
153
+ - Improved error handling for recording start/stop operations
154
+ - Better modal state cleanup prevents data corruption between sessions
155
+
156
+ ---
157
+
158
+ ## 6. Generate & Upload Installer
159
+
160
+ 1. **Build your app**:
161
+ * **Android**: `./gradlew assembleRelease` (outputs `.apk`)
162
+ * **iOS**: Archive via Xcode (outputs `.ipa`)
163
+ 2. **Upload**:
164
+ * Go back to your Project Dashboard on IInstall.
165
+ * Click **"New Build"** or **"Upload"**.
166
+ * Drag and drop your `.apk` or `.ipa` file.
167
+
168
+ Once uploaded, your testers can download the app. When they shake their device, the SDK will use the baked-in API Key to send feedback directly to your dashboard.
169
+
170
+ ---
171
+
172
+ ## Frequently Asked Questions
173
+
174
+ **Q: Do I need to change the API Key for every new build?**
175
+ A: **No.** The API Key stays the same for the life of the project. You just keep releasing new versions with the same key.
176
+
177
+ **Q: What happens if I upload a build without the SDK?**
178
+ A: The app will work fine, but features like "Shake to Report" and automatic session tracking will not work.
179
+
180
+ **Q: I can't see the project icons on the dashboard?**
181
+ A: Ensure your project icon URL is valid. If the image fails to load, the dashboard now automatically falls back to showing the project's initial letter on a styled background.
182
+
183
+ **Q: Audio feedback shows 0:00 duration in dashboard?**
184
+ A: Update to SDK v0.2.7+ which fixes audio codec compatibility. Audio files are now encoded as AAC instead of ALAC for better browser compatibility.
185
+
186
+ **Q: Screen recording sometimes doesn't capture?**
187
+ A: Update to SDK v0.2.7+ which includes enhanced URI extraction and better error handling for screen recordings across different devices.
package/README.md CHANGED
@@ -79,3 +79,4 @@ Add to `Info.plist`:
79
79
 
80
80
  - **Shake not working?** Ensure you are testing on a real device or enabling "Shake" in the simulator menu.
81
81
  - **Network errors?** Check if `apiEndpoint` is reachable and `apiKey` is correct.
82
+ - `apiEndpoint` must be a base URL (example: `https://iinstall.app`), not a route like `/api/sdk/issue`.
@@ -40,7 +40,7 @@ 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 react_native_audio_recorder_player_1 = __importDefault(require("react-native-audio-recorder-player"));
43
+ const react_native_audio_recorder_player_1 = __importStar(require("react-native-audio-recorder-player"));
44
44
  const FeedbackModal = ({ visible, onClose, screenshotUri, videoUri, onStartRecording, apiKey, apiEndpoint }) => {
45
45
  const [description, setDescription] = (0, react_1.useState)('');
46
46
  const [isSending, setIsSending] = (0, react_1.useState)(false);
@@ -77,7 +77,20 @@ const FeedbackModal = ({ visible, onClose, screenshotUri, videoUri, onStartRecor
77
77
  }
78
78
  }
79
79
  try {
80
- const result = await audioRecorderPlayer.startRecorder();
80
+ const audioSet = {
81
+ AVFormatIDKeyIOS: react_native_audio_recorder_player_1.AVEncodingOption.aac,
82
+ AVEncoderAudioQualityKeyIOS: react_native_audio_recorder_player_1.AVEncoderAudioQualityIOSType.high,
83
+ AVSampleRateKeyIOS: 44100,
84
+ AVNumberOfChannelsKeyIOS: 1,
85
+ AVEncoderBitRateKeyIOS: 128000,
86
+ AudioEncoderAndroid: react_native_audio_recorder_player_1.AudioEncoderAndroidType.AAC,
87
+ AudioSourceAndroid: react_native_audio_recorder_player_1.AudioSourceAndroidType.MIC,
88
+ OutputFormatAndroid: react_native_audio_recorder_player_1.OutputFormatAndroidType.MPEG_4,
89
+ AudioEncodingBitRateAndroid: 128000,
90
+ AudioSamplingRateAndroid: 44100,
91
+ AudioChannelsAndroid: 1,
92
+ };
93
+ const result = await audioRecorderPlayer.startRecorder(undefined, audioSet);
81
94
  audioRecorderPlayer.addRecordBackListener((e) => {
82
95
  setAudioDuration(audioRecorderPlayer.mmssss(Math.floor(e.currentPosition)));
83
96
  return;
@@ -101,6 +114,27 @@ const FeedbackModal = ({ visible, onClose, screenshotUri, videoUri, onStartRecor
101
114
  console.error('Failed to stop recording', error);
102
115
  }
103
116
  };
117
+ const resetDraft = () => {
118
+ setDescription('');
119
+ setAudioUri(null);
120
+ setAudioDuration('00:00');
121
+ };
122
+ const handleClose = async () => {
123
+ if (isRecordingAudio) {
124
+ try {
125
+ await audioRecorderPlayer.stopRecorder();
126
+ }
127
+ catch (error) {
128
+ console.warn('Failed to stop recorder during close', error);
129
+ }
130
+ finally {
131
+ audioRecorderPlayer.removeRecordBackListener();
132
+ setIsRecordingAudio(false);
133
+ }
134
+ }
135
+ resetDraft();
136
+ onClose();
137
+ };
104
138
  const sendFeedback = async () => {
105
139
  if (!description.trim() && !audioUri && !videoUri) {
106
140
  react_native_1.Alert.alert('Please provide a description, audio, or video.');
@@ -109,6 +143,12 @@ const FeedbackModal = ({ visible, onClose, screenshotUri, videoUri, onStartRecor
109
143
  setIsSending(true);
110
144
  try {
111
145
  const formData = new FormData();
146
+ const normalizeUploadUri = (uri) => {
147
+ if (react_native_1.Platform.OS === 'ios' && uri.startsWith('/')) {
148
+ return `file://${uri}`;
149
+ }
150
+ return uri;
151
+ };
112
152
  // 1. Screenshot
113
153
  if (screenshotUri) {
114
154
  const filename = screenshotUri.split('/').pop();
@@ -122,18 +162,22 @@ const FeedbackModal = ({ visible, onClose, screenshotUri, videoUri, onStartRecor
122
162
  }
123
163
  // 2. Audio
124
164
  if (audioUri) {
165
+ const normalizedAudioUri = normalizeUploadUri(audioUri);
125
166
  formData.append('audio', {
126
- uri: audioUri,
167
+ uri: normalizedAudioUri,
127
168
  name: 'feedback.m4a',
128
- type: 'audio/m4a',
169
+ type: 'audio/mp4',
129
170
  });
130
171
  }
131
172
  // 3. Video
132
173
  if (videoUri) {
174
+ const normalizedVideoUri = normalizeUploadUri(videoUri);
175
+ const videoFilename = normalizedVideoUri.split('/').pop() || 'screen_record.mp4';
176
+ const videoType = /\.mov$/i.test(videoFilename) ? 'video/quicktime' : 'video/mp4';
133
177
  formData.append('video', {
134
- uri: videoUri,
135
- name: 'screen_record.mp4',
136
- type: 'video/mp4',
178
+ uri: normalizedVideoUri,
179
+ name: videoFilename,
180
+ type: videoType,
137
181
  });
138
182
  }
139
183
  // 4. Metadata
@@ -159,9 +203,7 @@ const FeedbackModal = ({ visible, onClose, screenshotUri, videoUri, onStartRecor
159
203
  const result = await response.json();
160
204
  if (response.ok) {
161
205
  react_native_1.Alert.alert('Success', 'Feedback sent successfully!');
162
- setDescription('');
163
- setAudioUri(null);
164
- setAudioDuration('00:00');
206
+ resetDraft();
165
207
  onClose();
166
208
  }
167
209
  else {
@@ -178,12 +220,12 @@ const FeedbackModal = ({ visible, onClose, screenshotUri, videoUri, onStartRecor
178
220
  };
179
221
  if (!visible)
180
222
  return null;
181
- return (<react_native_1.Modal visible={visible} transparent animationType="slide" onRequestClose={onClose}>
223
+ return (<react_native_1.Modal visible={visible} transparent animationType="slide" onRequestClose={handleClose}>
182
224
  <react_native_1.KeyboardAvoidingView behavior={react_native_1.Platform.OS === 'ios' ? 'padding' : 'height'} style={styles.container}>
183
225
  <react_native_1.View style={styles.card}>
184
226
  <react_native_1.View style={styles.header}>
185
227
  <react_native_1.Text style={styles.title}>Report an Issue</react_native_1.Text>
186
- <react_native_1.TouchableOpacity onPress={onClose}>
228
+ <react_native_1.TouchableOpacity onPress={handleClose}>
187
229
  <react_native_1.Text style={styles.closeBtn}>✕</react_native_1.Text>
188
230
  </react_native_1.TouchableOpacity>
189
231
  </react_native_1.View>
package/lib/index.js CHANGED
@@ -89,11 +89,54 @@ const IInstall = ({ apiKey, apiEndpoint = 'https://iinstall.app', children, enab
89
89
  shakeDetectorRef.current?.stop();
90
90
  };
91
91
  }, [enabled]);
92
+ const normalizeVideoUri = (value) => {
93
+ if (react_native_1.Platform.OS === 'ios' && value.startsWith('/')) {
94
+ return `file://${value}`;
95
+ }
96
+ return value;
97
+ };
98
+ const resolveRecordedVideoUri = (recordResult) => {
99
+ if (!recordResult || typeof recordResult !== 'object') {
100
+ return null;
101
+ }
102
+ const candidate = recordResult;
103
+ const values = [
104
+ candidate?.result?.outputURL,
105
+ candidate?.result?.outputUrl,
106
+ candidate?.result?.outputUri,
107
+ candidate?.result?.outputFileURL,
108
+ candidate?.result?.outputFileUri,
109
+ candidate?.result?.uri,
110
+ candidate?.result?.path,
111
+ candidate?.result?.filePath,
112
+ candidate?.result?.fileURL,
113
+ candidate?.outputURL,
114
+ candidate?.outputUrl,
115
+ candidate?.outputUri,
116
+ candidate?.outputFileURL,
117
+ candidate?.outputFileUri,
118
+ candidate?.uri,
119
+ candidate?.path,
120
+ candidate?.filePath,
121
+ candidate?.fileURL,
122
+ ];
123
+ for (const value of values) {
124
+ if (typeof value === 'string' && value.trim().length > 0) {
125
+ return normalizeVideoUri(value);
126
+ }
127
+ }
128
+ for (const value of Object.values(candidate)) {
129
+ if (typeof value === 'string' && value.trim().length > 0) {
130
+ return normalizeVideoUri(value);
131
+ }
132
+ }
133
+ return null;
134
+ };
92
135
  const handleStartRecording = async () => {
93
136
  setModalVisible(false);
94
137
  setIsRecording(true);
95
138
  try {
96
- await react_native_record_screen_1.default.startRecording({ mic: true }).catch((error) => console.error(error));
139
+ await react_native_record_screen_1.default.startRecording({ mic: true });
97
140
  }
98
141
  catch (e) {
99
142
  console.error('Failed to start recording', e);
@@ -104,16 +147,26 @@ const IInstall = ({ apiKey, apiEndpoint = 'https://iinstall.app', children, enab
104
147
  const handleStopRecording = async () => {
105
148
  try {
106
149
  // 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);
150
+ const res = await react_native_record_screen_1.default.stopRecording();
109
151
  if (res) {
110
- setVideoUri(res.result.outputURL);
111
- setModalVisible(true);
152
+ const nextVideoUri = resolveRecordedVideoUri(res);
153
+ if (nextVideoUri) {
154
+ setVideoUri(nextVideoUri);
155
+ }
156
+ else {
157
+ console.warn('IInstall: Screen recording finished, but no video URI was returned.', res);
158
+ }
159
+ }
160
+ else {
161
+ console.warn('IInstall: stopRecording returned empty result');
112
162
  }
113
163
  }
114
164
  catch (e) {
115
165
  console.error('Failed to stop recording', e);
166
+ }
167
+ finally {
116
168
  setIsRecording(false);
169
+ setModalVisible(true);
117
170
  }
118
171
  };
119
172
  return (<react_native_1.View style={react_native_1.StyleSheet.absoluteFill} pointerEvents="box-none">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-iinstall",
3
- "version": "0.2.3",
3
+ "version": "0.2.7",
4
4
  "description": "IInstall SDK for React Native - Shake to Report with Audio & Video",
5
5
  "author": "TesterFlow Team",
6
6
  "license": "MIT",
@@ -19,7 +19,8 @@
19
19
  "types": "lib/index.d.ts",
20
20
  "files": [
21
21
  "lib",
22
- "src"
22
+ "src",
23
+ "INTEGRATION_GUIDE.md"
23
24
  ],
24
25
  "scripts": {
25
26
  "build": "tsc",
@@ -14,7 +14,14 @@ import {
14
14
  PermissionsAndroid
15
15
  } from 'react-native';
16
16
  import DeviceInfo from 'react-native-device-info';
17
- import AudioRecorderPlayer from 'react-native-audio-recorder-player';
17
+ import AudioRecorderPlayer, {
18
+ AVEncodingOption,
19
+ AVEncoderAudioQualityIOSType,
20
+ AudioEncoderAndroidType,
21
+ AudioSourceAndroidType,
22
+ OutputFormatAndroidType,
23
+ type AudioSet,
24
+ } from 'react-native-audio-recorder-player';
18
25
 
19
26
  interface FeedbackModalProps {
20
27
  visible: boolean;
@@ -76,7 +83,24 @@ export const FeedbackModal: React.FC<FeedbackModalProps> = ({
76
83
  }
77
84
 
78
85
  try {
79
- const result = await audioRecorderPlayer.startRecorder();
86
+ const audioSet: AudioSet = {
87
+ AVFormatIDKeyIOS: AVEncodingOption.aac,
88
+ AVEncoderAudioQualityKeyIOS: AVEncoderAudioQualityIOSType.high,
89
+ AVSampleRateKeyIOS: 44100,
90
+ AVNumberOfChannelsKeyIOS: 1,
91
+ AVEncoderBitRateKeyIOS: 128000,
92
+ AudioEncoderAndroid: AudioEncoderAndroidType.AAC,
93
+ AudioSourceAndroid: AudioSourceAndroidType.MIC,
94
+ OutputFormatAndroid: OutputFormatAndroidType.MPEG_4,
95
+ AudioEncodingBitRateAndroid: 128000,
96
+ AudioSamplingRateAndroid: 44100,
97
+ AudioChannelsAndroid: 1,
98
+ };
99
+
100
+ const result = await audioRecorderPlayer.startRecorder(
101
+ undefined,
102
+ audioSet,
103
+ );
80
104
  audioRecorderPlayer.addRecordBackListener((e) => {
81
105
  setAudioDuration(audioRecorderPlayer.mmssss(Math.floor(e.currentPosition)));
82
106
  return;
@@ -100,6 +124,28 @@ export const FeedbackModal: React.FC<FeedbackModalProps> = ({
100
124
  }
101
125
  };
102
126
 
127
+ const resetDraft = () => {
128
+ setDescription('');
129
+ setAudioUri(null);
130
+ setAudioDuration('00:00');
131
+ };
132
+
133
+ const handleClose = async () => {
134
+ if (isRecordingAudio) {
135
+ try {
136
+ await audioRecorderPlayer.stopRecorder();
137
+ } catch (error) {
138
+ console.warn('Failed to stop recorder during close', error);
139
+ } finally {
140
+ audioRecorderPlayer.removeRecordBackListener();
141
+ setIsRecordingAudio(false);
142
+ }
143
+ }
144
+
145
+ resetDraft();
146
+ onClose();
147
+ };
148
+
103
149
  const sendFeedback = async () => {
104
150
  if (!description.trim() && !audioUri && !videoUri) {
105
151
  Alert.alert('Please provide a description, audio, or video.');
@@ -110,6 +156,12 @@ export const FeedbackModal: React.FC<FeedbackModalProps> = ({
110
156
 
111
157
  try {
112
158
  const formData = new FormData();
159
+ const normalizeUploadUri = (uri: string) => {
160
+ if (Platform.OS === 'ios' && uri.startsWith('/')) {
161
+ return `file://${uri}`;
162
+ }
163
+ return uri;
164
+ };
113
165
 
114
166
  // 1. Screenshot
115
167
  if (screenshotUri) {
@@ -126,19 +178,24 @@ export const FeedbackModal: React.FC<FeedbackModalProps> = ({
126
178
 
127
179
  // 2. Audio
128
180
  if (audioUri) {
181
+ const normalizedAudioUri = normalizeUploadUri(audioUri);
129
182
  formData.append('audio', {
130
- uri: audioUri,
183
+ uri: normalizedAudioUri,
131
184
  name: 'feedback.m4a',
132
- type: 'audio/m4a',
185
+ type: 'audio/mp4',
133
186
  } as any);
134
187
  }
135
188
 
136
189
  // 3. Video
137
190
  if (videoUri) {
191
+ const normalizedVideoUri = normalizeUploadUri(videoUri);
192
+ const videoFilename = normalizedVideoUri.split('/').pop() || 'screen_record.mp4';
193
+ const videoType = /\.mov$/i.test(videoFilename) ? 'video/quicktime' : 'video/mp4';
194
+
138
195
  formData.append('video', {
139
- uri: videoUri,
140
- name: 'screen_record.mp4',
141
- type: 'video/mp4',
196
+ uri: normalizedVideoUri,
197
+ name: videoFilename,
198
+ type: videoType,
142
199
  } as any);
143
200
  }
144
201
 
@@ -169,9 +226,7 @@ export const FeedbackModal: React.FC<FeedbackModalProps> = ({
169
226
 
170
227
  if (response.ok) {
171
228
  Alert.alert('Success', 'Feedback sent successfully!');
172
- setDescription('');
173
- setAudioUri(null);
174
- setAudioDuration('00:00');
229
+ resetDraft();
175
230
  onClose();
176
231
  } else {
177
232
  throw new Error(result.error || 'Failed to send');
@@ -188,7 +243,7 @@ export const FeedbackModal: React.FC<FeedbackModalProps> = ({
188
243
  if (!visible) return null;
189
244
 
190
245
  return (
191
- <Modal visible={visible} transparent animationType="slide" onRequestClose={onClose}>
246
+ <Modal visible={visible} transparent animationType="slide" onRequestClose={handleClose}>
192
247
  <KeyboardAvoidingView
193
248
  behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
194
249
  style={styles.container}
@@ -196,7 +251,7 @@ export const FeedbackModal: React.FC<FeedbackModalProps> = ({
196
251
  <View style={styles.card}>
197
252
  <View style={styles.header}>
198
253
  <Text style={styles.title}>Report an Issue</Text>
199
- <TouchableOpacity onPress={onClose}>
254
+ <TouchableOpacity onPress={handleClose}>
200
255
  <Text style={styles.closeBtn}>✕</Text>
201
256
  </TouchableOpacity>
202
257
  </View>
package/src/index.tsx CHANGED
@@ -70,11 +70,83 @@ export const IInstall: React.FC<IInstallProps> = ({
70
70
  };
71
71
  }, [enabled]);
72
72
 
73
+ const normalizeVideoUri = (value: string) => {
74
+ if (Platform.OS === 'ios' && value.startsWith('/')) {
75
+ return `file://${value}`;
76
+ }
77
+ return value;
78
+ };
79
+
80
+ const resolveRecordedVideoUri = (recordResult: unknown): string | null => {
81
+ if (!recordResult || typeof recordResult !== 'object') {
82
+ return null;
83
+ }
84
+
85
+ const candidate = recordResult as
86
+ {
87
+ result?: {
88
+ outputURL?: string;
89
+ outputUrl?: string;
90
+ outputUri?: string;
91
+ outputFileURL?: string;
92
+ outputFileUri?: string;
93
+ uri?: string;
94
+ path?: string;
95
+ filePath?: string;
96
+ fileURL?: string;
97
+ };
98
+ outputURL?: string;
99
+ outputUrl?: string;
100
+ outputUri?: string;
101
+ outputFileURL?: string;
102
+ outputFileUri?: string;
103
+ uri?: string;
104
+ path?: string;
105
+ filePath?: string;
106
+ fileURL?: string;
107
+ };
108
+
109
+ const values = [
110
+ candidate?.result?.outputURL,
111
+ candidate?.result?.outputUrl,
112
+ candidate?.result?.outputUri,
113
+ candidate?.result?.outputFileURL,
114
+ candidate?.result?.outputFileUri,
115
+ candidate?.result?.uri,
116
+ candidate?.result?.path,
117
+ candidate?.result?.filePath,
118
+ candidate?.result?.fileURL,
119
+ candidate?.outputURL,
120
+ candidate?.outputUrl,
121
+ candidate?.outputUri,
122
+ candidate?.outputFileURL,
123
+ candidate?.outputFileUri,
124
+ candidate?.uri,
125
+ candidate?.path,
126
+ candidate?.filePath,
127
+ candidate?.fileURL,
128
+ ];
129
+
130
+ for (const value of values) {
131
+ if (typeof value === 'string' && value.trim().length > 0) {
132
+ return normalizeVideoUri(value);
133
+ }
134
+ }
135
+
136
+ for (const value of Object.values(candidate)) {
137
+ if (typeof value === 'string' && value.trim().length > 0) {
138
+ return normalizeVideoUri(value);
139
+ }
140
+ }
141
+
142
+ return null;
143
+ };
144
+
73
145
  const handleStartRecording = async () => {
74
146
  setModalVisible(false);
75
147
  setIsRecording(true);
76
148
  try {
77
- await RecordScreen.startRecording({ mic: true }).catch((error: any) => console.error(error));
149
+ await RecordScreen.startRecording({ mic: true });
78
150
  } catch (e) {
79
151
  console.error('Failed to start recording', e);
80
152
  setIsRecording(false);
@@ -85,15 +157,22 @@ export const IInstall: React.FC<IInstallProps> = ({
85
157
  const handleStopRecording = async () => {
86
158
  try {
87
159
  // 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);
160
+ const res: any = await RecordScreen.stopRecording();
90
161
  if (res) {
91
- setVideoUri(res.result.outputURL);
92
- setModalVisible(true);
162
+ const nextVideoUri = resolveRecordedVideoUri(res);
163
+ if (nextVideoUri) {
164
+ setVideoUri(nextVideoUri);
165
+ } else {
166
+ console.warn('IInstall: Screen recording finished, but no video URI was returned.', res);
167
+ }
168
+ } else {
169
+ console.warn('IInstall: stopRecording returned empty result');
93
170
  }
94
171
  } catch (e) {
95
172
  console.error('Failed to stop recording', e);
173
+ } finally {
96
174
  setIsRecording(false);
175
+ setModalVisible(true);
97
176
  }
98
177
  };
99
178