react-native-iinstall 0.2.13 → 0.2.15
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/INTEGRATION_GUIDE.md +12 -7
- package/README.md +7 -3
- package/RELEASE_MANIFEST.json +8 -0
- package/lib/FeedbackModal.js +34 -36
- package/lib/index.js +13 -12
- package/lib/nativeModules.d.ts +37 -0
- package/lib/nativeModules.js +232 -0
- package/package.json +20 -9
- package/src/FeedbackModal.tsx +46 -41
- package/src/index.tsx +23 -9
- package/src/nativeModules.ts +313 -0
package/INTEGRATION_GUIDE.md
CHANGED
|
@@ -46,16 +46,20 @@ npm install react-native-iinstall
|
|
|
46
46
|
yarn add react-native-iinstall
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
Install required native peer dependencies at app root:
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
```bash
|
|
52
|
+
npm install react-native-sensors react-native-view-shot react-native-device-info react-native-audio-recorder-player react-native-record-screen
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Updating to Latest Version (v0.2.14)
|
|
52
56
|
|
|
53
57
|
If you're using an older version, update to get the latest audio/video improvements:
|
|
54
58
|
|
|
55
59
|
```bash
|
|
56
|
-
npm install react-native-iinstall@0.2.
|
|
60
|
+
npm install react-native-iinstall@0.2.14
|
|
57
61
|
# OR
|
|
58
|
-
yarn add react-native-iinstall@0.2.
|
|
62
|
+
yarn add react-native-iinstall@0.2.14
|
|
59
63
|
```
|
|
60
64
|
|
|
61
65
|
**Key improvements in v0.2.7:**
|
|
@@ -136,7 +140,7 @@ import { registerPushToken, unregisterPushToken } from 'react-native-iinstall';
|
|
|
136
140
|
|
|
137
141
|
---
|
|
138
142
|
|
|
139
|
-
## 6. Audio & Video Feedback (v0.2.
|
|
143
|
+
## 6. Audio & Video Feedback (v0.2.14+)
|
|
140
144
|
|
|
141
145
|
The SDK now supports **Audio Feedback** (Voice Notes) and **Screen Recording** with enhanced compatibility and reliability.
|
|
142
146
|
|
|
@@ -151,6 +155,7 @@ The SDK now supports **Audio Feedback** (Voice Notes) and **Screen Recording** w
|
|
|
151
155
|
These features require native modules. If you haven't already, run:
|
|
152
156
|
|
|
153
157
|
```bash
|
|
158
|
+
npm install react-native-sensors react-native-view-shot react-native-device-info react-native-audio-recorder-player react-native-record-screen
|
|
154
159
|
cd ios && pod install
|
|
155
160
|
```
|
|
156
161
|
|
|
@@ -219,7 +224,7 @@ A: The app will work fine, but features like "Shake to Report" and automatic ses
|
|
|
219
224
|
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.
|
|
220
225
|
|
|
221
226
|
**Q: Audio feedback shows 0:00 duration in dashboard?**
|
|
222
|
-
A: Update to SDK v0.2.
|
|
227
|
+
A: Update to SDK v0.2.14+ which includes the audio codec compatibility fixes. Audio files are encoded as AAC for browser playback.
|
|
223
228
|
|
|
224
229
|
**Q: Screen recording sometimes doesn't capture?**
|
|
225
|
-
A: Update to SDK v0.2.
|
|
230
|
+
A: Update to SDK v0.2.14+ which includes enhanced URI extraction and better error handling for screen recordings across different devices.
|
package/README.md
CHANGED
|
@@ -34,9 +34,12 @@ Transform your app's feedback collection with our powerful shake-to-report SDK.
|
|
|
34
34
|
npm install react-native-iinstall
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
2. Install required native peer dependencies in your app root:
|
|
38
|
+
```bash
|
|
39
|
+
npm install react-native-sensors react-native-view-shot react-native-device-info react-native-audio-recorder-player react-native-record-screen
|
|
40
|
+
```
|
|
38
41
|
|
|
39
|
-
|
|
42
|
+
3. Link native modules (iOS only, non-Expo):
|
|
40
43
|
```bash
|
|
41
44
|
cd ios && pod install
|
|
42
45
|
```
|
|
@@ -173,7 +176,8 @@ await unregisterPushToken({
|
|
|
173
176
|
- **Shake not working?** Test on real device or enable "Shake" in simulator
|
|
174
177
|
- **Network errors?** Verify `apiEndpoint` is base URL only (not `/api/sdk/issue`)
|
|
175
178
|
- **Permissions denied?** Check platform-specific setup in integration guide
|
|
176
|
-
- **Audio issues?** Ensure SDK v0.2.
|
|
179
|
+
- **Audio issues?** Ensure SDK v0.2.14+ and rebuild after native peer install
|
|
180
|
+
- **Native module is null/undefined?** Reinstall peer deps in app root, run `cd ios && pod install`, then rebuild the app
|
|
177
181
|
|
|
178
182
|
### Getting Help
|
|
179
183
|
- 📖 [Complete Integration Guide](INTEGRATION_GUIDE.md)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "0.2.15",
|
|
3
|
+
"highlights": [
|
|
4
|
+
"Native modules now declared as peer dependencies for reliable app-root autolinking",
|
|
5
|
+
"SDK now lazy-loads native modules and degrades gracefully instead of hard crashing",
|
|
6
|
+
"Updated integration docs with explicit peer dependency install commands"
|
|
7
|
+
]
|
|
8
|
+
}
|
package/lib/FeedbackModal.js
CHANGED
|
@@ -32,15 +32,11 @@ 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
|
-
};
|
|
38
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
36
|
exports.FeedbackModal = void 0;
|
|
40
37
|
const react_1 = __importStar(require("react"));
|
|
41
38
|
const react_native_1 = require("react-native");
|
|
42
|
-
const
|
|
43
|
-
const react_native_audio_recorder_player_1 = __importStar(require("react-native-audio-recorder-player"));
|
|
39
|
+
const nativeModules_1 = require("./nativeModules");
|
|
44
40
|
const FeedbackModal = ({ visible, onClose, screenshotUri, videoUri, onStartRecording, apiKey, apiEndpoint }) => {
|
|
45
41
|
const [description, setDescription] = (0, react_1.useState)('');
|
|
46
42
|
const [isSending, setIsSending] = (0, react_1.useState)(false);
|
|
@@ -48,16 +44,21 @@ const FeedbackModal = ({ visible, onClose, screenshotUri, videoUri, onStartRecor
|
|
|
48
44
|
const [isRecordingAudio, setIsRecordingAudio] = (0, react_1.useState)(false);
|
|
49
45
|
const [audioUri, setAudioUri] = (0, react_1.useState)(null);
|
|
50
46
|
const [audioDuration, setAudioDuration] = (0, react_1.useState)('00:00');
|
|
51
|
-
const
|
|
47
|
+
const audioRecordingAvailable = (0, nativeModules_1.hasAudioRecordingSupport)();
|
|
48
|
+
const audioRecorderPlayer = (0, react_1.useRef)((0, nativeModules_1.createAudioRecorderPlayer)()).current;
|
|
52
49
|
(0, react_1.useEffect)(() => {
|
|
53
50
|
return () => {
|
|
54
51
|
// Cleanup
|
|
55
|
-
if (isRecordingAudio) {
|
|
56
|
-
audioRecorderPlayer.stopRecorder();
|
|
52
|
+
if (isRecordingAudio && audioRecorderPlayer) {
|
|
53
|
+
audioRecorderPlayer.stopRecorder().catch(() => undefined);
|
|
57
54
|
}
|
|
58
55
|
};
|
|
59
|
-
}, [isRecordingAudio]);
|
|
56
|
+
}, [audioRecorderPlayer, isRecordingAudio]);
|
|
60
57
|
const onStartAudioRecord = async () => {
|
|
58
|
+
if (!audioRecorderPlayer || !audioRecordingAvailable) {
|
|
59
|
+
react_native_1.Alert.alert('Audio unavailable', 'Install react-native-audio-recorder-player in your app root, then rebuild the app.');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
61
62
|
if (react_native_1.Platform.OS === 'android') {
|
|
62
63
|
try {
|
|
63
64
|
const grants = await react_native_1.PermissionsAndroid.requestMultiple([
|
|
@@ -77,20 +78,7 @@ const FeedbackModal = ({ visible, onClose, screenshotUri, videoUri, onStartRecor
|
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
80
|
try {
|
|
80
|
-
const
|
|
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
|
+
const result = await audioRecorderPlayer.startRecorder(undefined, (0, nativeModules_1.getAudioRecordingPreset)());
|
|
94
82
|
audioRecorderPlayer.addRecordBackListener((e) => {
|
|
95
83
|
setAudioDuration(audioRecorderPlayer.mmssss(Math.floor(e.currentPosition)));
|
|
96
84
|
return;
|
|
@@ -104,6 +92,9 @@ const FeedbackModal = ({ visible, onClose, screenshotUri, videoUri, onStartRecor
|
|
|
104
92
|
}
|
|
105
93
|
};
|
|
106
94
|
const onStopAudioRecord = async () => {
|
|
95
|
+
if (!audioRecorderPlayer) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
107
98
|
try {
|
|
108
99
|
const result = await audioRecorderPlayer.stopRecorder();
|
|
109
100
|
audioRecorderPlayer.removeRecordBackListener();
|
|
@@ -120,7 +111,7 @@ const FeedbackModal = ({ visible, onClose, screenshotUri, videoUri, onStartRecor
|
|
|
120
111
|
setAudioDuration('00:00');
|
|
121
112
|
};
|
|
122
113
|
const handleClose = async () => {
|
|
123
|
-
if (isRecordingAudio) {
|
|
114
|
+
if (isRecordingAudio && audioRecorderPlayer) {
|
|
124
115
|
try {
|
|
125
116
|
await audioRecorderPlayer.stopRecorder();
|
|
126
117
|
}
|
|
@@ -181,20 +172,13 @@ const FeedbackModal = ({ visible, onClose, screenshotUri, videoUri, onStartRecor
|
|
|
181
172
|
});
|
|
182
173
|
}
|
|
183
174
|
// 4. Metadata
|
|
184
|
-
const metadata =
|
|
185
|
-
device: react_native_device_info_1.default.getModel(),
|
|
186
|
-
systemName: react_native_device_info_1.default.getSystemName(),
|
|
187
|
-
systemVersion: react_native_device_info_1.default.getSystemVersion(),
|
|
188
|
-
appVersion: react_native_device_info_1.default.getVersion(),
|
|
189
|
-
buildNumber: react_native_device_info_1.default.getBuildNumber(),
|
|
190
|
-
brand: react_native_device_info_1.default.getBrand(),
|
|
191
|
-
isEmulator: await react_native_device_info_1.default.isEmulator(),
|
|
192
|
-
};
|
|
175
|
+
const metadata = await (0, nativeModules_1.getDeviceMetadata)();
|
|
193
176
|
formData.append('metadata', JSON.stringify(metadata));
|
|
194
177
|
// 5. Other fields
|
|
195
178
|
formData.append('description', description);
|
|
196
179
|
formData.append('apiKey', apiKey);
|
|
197
|
-
|
|
180
|
+
const deviceUdid = await (0, nativeModules_1.getDeviceUniqueId)();
|
|
181
|
+
formData.append('udid', deviceUdid || 'unknown');
|
|
198
182
|
// 6. Send
|
|
199
183
|
const response = await fetch(`${apiEndpoint}/api/feedback`, {
|
|
200
184
|
method: 'POST',
|
|
@@ -257,9 +241,20 @@ const FeedbackModal = ({ visible, onClose, screenshotUri, videoUri, onStartRecor
|
|
|
257
241
|
{/* Media Actions Row */}
|
|
258
242
|
<react_native_1.View style={styles.actionsRow}>
|
|
259
243
|
{/* Audio Recorder */}
|
|
260
|
-
<react_native_1.TouchableOpacity style={[
|
|
244
|
+
<react_native_1.TouchableOpacity style={[
|
|
245
|
+
styles.actionBtn,
|
|
246
|
+
isRecordingAudio ? styles.recordingBtn : undefined,
|
|
247
|
+
audioUri ? styles.hasAudioBtn : undefined,
|
|
248
|
+
!audioRecordingAvailable ? styles.disabledActionBtn : undefined,
|
|
249
|
+
]} onPress={isRecordingAudio ? onStopAudioRecord : onStartAudioRecord} disabled={!audioRecordingAvailable}>
|
|
261
250
|
<react_native_1.Text style={[styles.actionBtnText, (isRecordingAudio || audioUri) ? { color: '#FFF' } : undefined]}>
|
|
262
|
-
{
|
|
251
|
+
{!audioRecordingAvailable
|
|
252
|
+
? '🎙 Audio Unavailable'
|
|
253
|
+
: isRecordingAudio
|
|
254
|
+
? `Stop (${audioDuration})`
|
|
255
|
+
: audioUri
|
|
256
|
+
? 'Re-record Audio'
|
|
257
|
+
: '🎙 Record Audio'}
|
|
263
258
|
</react_native_1.Text>
|
|
264
259
|
</react_native_1.TouchableOpacity>
|
|
265
260
|
|
|
@@ -372,6 +367,9 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
372
367
|
hasAudioBtn: {
|
|
373
368
|
backgroundColor: '#4CAF50',
|
|
374
369
|
},
|
|
370
|
+
disabledActionBtn: {
|
|
371
|
+
opacity: 0.45,
|
|
372
|
+
},
|
|
375
373
|
actionBtnText: {
|
|
376
374
|
fontSize: 14,
|
|
377
375
|
fontWeight: '600',
|
package/lib/index.js
CHANGED
|
@@ -32,19 +32,14 @@ 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
|
-
};
|
|
38
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
36
|
exports.unregisterPushToken = exports.registerPushToken = exports.IInstallWrapper = exports.IInstall = void 0;
|
|
40
37
|
const react_1 = __importStar(require("react"));
|
|
41
38
|
const react_native_1 = require("react-native");
|
|
42
|
-
const react_native_view_shot_1 = require("react-native-view-shot");
|
|
43
39
|
const ShakeDetector_1 = require("./ShakeDetector");
|
|
44
40
|
const FeedbackModal_1 = require("./FeedbackModal");
|
|
45
|
-
const react_native_record_screen_1 = __importDefault(require("react-native-record-screen"));
|
|
46
|
-
const react_native_device_info_1 = __importDefault(require("react-native-device-info"));
|
|
47
41
|
const pushRegistration_1 = require("./pushRegistration");
|
|
42
|
+
const nativeModules_1 = require("./nativeModules");
|
|
48
43
|
const IInstall = ({ apiKey, apiEndpoint = 'https://iinstall.app', children, enabled = true, showFloatingButtonOnEmulator = true, floatingButtonLabel = 'Report Issue', pushToken, autoRegisterPushToken = true, projectId, onPushTokenRegisterError, }) => {
|
|
49
44
|
const [modalVisible, setModalVisible] = (0, react_1.useState)(false);
|
|
50
45
|
const [screenshotUri, setScreenshotUri] = (0, react_1.useState)(null);
|
|
@@ -62,7 +57,7 @@ const IInstall = ({ apiKey, apiEndpoint = 'https://iinstall.app', children, enab
|
|
|
62
57
|
}, [isRecording, modalVisible]);
|
|
63
58
|
(0, react_1.useEffect)(() => {
|
|
64
59
|
let mounted = true;
|
|
65
|
-
|
|
60
|
+
(0, nativeModules_1.isEmulatorDevice)()
|
|
66
61
|
.then((value) => {
|
|
67
62
|
if (mounted) {
|
|
68
63
|
setIsEmulator(value);
|
|
@@ -82,7 +77,7 @@ const IInstall = ({ apiKey, apiEndpoint = 'https://iinstall.app', children, enab
|
|
|
82
77
|
return;
|
|
83
78
|
try {
|
|
84
79
|
// Capture screenshot
|
|
85
|
-
const uri = await (0,
|
|
80
|
+
const uri = await (0, nativeModules_1.captureScreenImage)({
|
|
86
81
|
format: 'png',
|
|
87
82
|
quality: 0.8,
|
|
88
83
|
});
|
|
@@ -117,7 +112,7 @@ const IInstall = ({ apiKey, apiEndpoint = 'https://iinstall.app', children, enab
|
|
|
117
112
|
return;
|
|
118
113
|
let cancelled = false;
|
|
119
114
|
const syncPushToken = async () => {
|
|
120
|
-
const deviceUdid = await
|
|
115
|
+
const deviceUdid = await (0, nativeModules_1.getDeviceUniqueId)();
|
|
121
116
|
const result = await (0, pushRegistration_1.registerPushToken)({
|
|
122
117
|
token: pushToken,
|
|
123
118
|
apiKey,
|
|
@@ -167,6 +162,7 @@ const IInstall = ({ apiKey, apiEndpoint = 'https://iinstall.app', children, enab
|
|
|
167
162
|
showFloatingButtonOnEmulator &&
|
|
168
163
|
!modalVisible &&
|
|
169
164
|
!isRecording;
|
|
165
|
+
const screenRecordingAvailable = (0, nativeModules_1.hasScreenRecordingSupport)();
|
|
170
166
|
const normalizeVideoUri = (value) => {
|
|
171
167
|
if (react_native_1.Platform.OS === 'ios' && value.startsWith('/')) {
|
|
172
168
|
return `file://${value}`;
|
|
@@ -211,10 +207,15 @@ const IInstall = ({ apiKey, apiEndpoint = 'https://iinstall.app', children, enab
|
|
|
211
207
|
return null;
|
|
212
208
|
};
|
|
213
209
|
const handleStartRecording = async () => {
|
|
210
|
+
if (!screenRecordingAvailable) {
|
|
211
|
+
console.warn('IInstall: Screen recording unavailable. Install react-native-record-screen in your app and rebuild.');
|
|
212
|
+
setModalVisible(true);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
214
215
|
setModalVisible(false);
|
|
215
216
|
setIsRecording(true);
|
|
216
217
|
try {
|
|
217
|
-
await
|
|
218
|
+
await (0, nativeModules_1.startScreenRecording)({ mic: true });
|
|
218
219
|
}
|
|
219
220
|
catch (e) {
|
|
220
221
|
console.error('Failed to start recording', e);
|
|
@@ -225,7 +226,7 @@ const IInstall = ({ apiKey, apiEndpoint = 'https://iinstall.app', children, enab
|
|
|
225
226
|
const handleStopRecording = async () => {
|
|
226
227
|
try {
|
|
227
228
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
228
|
-
const res = await
|
|
229
|
+
const res = await (0, nativeModules_1.stopScreenRecording)();
|
|
229
230
|
if (res) {
|
|
230
231
|
const nextVideoUri = resolveRecordedVideoUri(res);
|
|
231
232
|
if (nextVideoUri) {
|
|
@@ -272,7 +273,7 @@ const IInstall = ({ apiKey, apiEndpoint = 'https://iinstall.app', children, enab
|
|
|
272
273
|
setModalVisible(false);
|
|
273
274
|
setVideoUri(null); // Clear video on close
|
|
274
275
|
setScreenshotUri(null);
|
|
275
|
-
}} screenshotUri={screenshotUri} videoUri={videoUri} onStartRecording={handleStartRecording} apiKey={apiKey} apiEndpoint={apiEndpoint}/>
|
|
276
|
+
}} screenshotUri={screenshotUri} videoUri={videoUri} onStartRecording={screenRecordingAvailable ? handleStartRecording : undefined} apiKey={apiKey} apiEndpoint={apiEndpoint}/>
|
|
276
277
|
</react_native_1.View>);
|
|
277
278
|
};
|
|
278
279
|
exports.IInstall = IInstall;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
type CaptureScreenOptions = {
|
|
2
|
+
format?: 'png' | 'jpg' | 'webm' | 'raw';
|
|
3
|
+
quality?: number;
|
|
4
|
+
result?: 'tmpfile' | 'base64' | 'data-uri' | 'zip-base64';
|
|
5
|
+
};
|
|
6
|
+
type RecordBackEvent = {
|
|
7
|
+
currentPosition: number;
|
|
8
|
+
};
|
|
9
|
+
export type AudioRecorderPlayerLike = {
|
|
10
|
+
startRecorder: (uri?: string, audioSets?: Record<string, unknown>) => Promise<string>;
|
|
11
|
+
stopRecorder: () => Promise<string>;
|
|
12
|
+
addRecordBackListener: (callback: (recordingMeta: RecordBackEvent) => void) => void;
|
|
13
|
+
removeRecordBackListener: () => void;
|
|
14
|
+
mmssss: (milisecs: number) => string;
|
|
15
|
+
};
|
|
16
|
+
export type DeviceMetadata = {
|
|
17
|
+
device: string;
|
|
18
|
+
systemName: string;
|
|
19
|
+
systemVersion: string;
|
|
20
|
+
appVersion: string;
|
|
21
|
+
buildNumber: string;
|
|
22
|
+
brand: string;
|
|
23
|
+
isEmulator: boolean;
|
|
24
|
+
};
|
|
25
|
+
export declare function isEmulatorDevice(): Promise<boolean>;
|
|
26
|
+
export declare function getDeviceUniqueId(): Promise<string | undefined>;
|
|
27
|
+
export declare function getDeviceMetadata(): Promise<DeviceMetadata>;
|
|
28
|
+
export declare function hasScreenRecordingSupport(): boolean;
|
|
29
|
+
export declare function startScreenRecording(config?: {
|
|
30
|
+
mic?: boolean;
|
|
31
|
+
}): Promise<void>;
|
|
32
|
+
export declare function stopScreenRecording(): Promise<unknown>;
|
|
33
|
+
export declare function captureScreenImage(options?: CaptureScreenOptions): Promise<string>;
|
|
34
|
+
export declare function hasAudioRecordingSupport(): boolean;
|
|
35
|
+
export declare function createAudioRecorderPlayer(): AudioRecorderPlayerLike | null;
|
|
36
|
+
export declare function getAudioRecordingPreset(): Record<string, string | number>;
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isEmulatorDevice = isEmulatorDevice;
|
|
4
|
+
exports.getDeviceUniqueId = getDeviceUniqueId;
|
|
5
|
+
exports.getDeviceMetadata = getDeviceMetadata;
|
|
6
|
+
exports.hasScreenRecordingSupport = hasScreenRecordingSupport;
|
|
7
|
+
exports.startScreenRecording = startScreenRecording;
|
|
8
|
+
exports.stopScreenRecording = stopScreenRecording;
|
|
9
|
+
exports.captureScreenImage = captureScreenImage;
|
|
10
|
+
exports.hasAudioRecordingSupport = hasAudioRecordingSupport;
|
|
11
|
+
exports.createAudioRecorderPlayer = createAudioRecorderPlayer;
|
|
12
|
+
exports.getAudioRecordingPreset = getAudioRecordingPreset;
|
|
13
|
+
const react_native_1 = require("react-native");
|
|
14
|
+
const warnedKeys = new Set();
|
|
15
|
+
function warnOnce(key, message) {
|
|
16
|
+
if (warnedKeys.has(key)) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
warnedKeys.add(key);
|
|
20
|
+
console.warn(message);
|
|
21
|
+
}
|
|
22
|
+
function getDefaultExport(moduleValue) {
|
|
23
|
+
if (!moduleValue) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
if (typeof moduleValue === 'object' && moduleValue !== null && 'default' in moduleValue) {
|
|
27
|
+
return moduleValue.default;
|
|
28
|
+
}
|
|
29
|
+
return moduleValue;
|
|
30
|
+
}
|
|
31
|
+
function loadRawModule(moduleName) {
|
|
32
|
+
try {
|
|
33
|
+
return require(moduleName);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
37
|
+
warnOnce(`missing:${moduleName}`, `[iInstall SDK] Missing native dependency "${moduleName}". Install it in your app root and rebuild. (${reason})`);
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
let cachedDeviceInfoModule;
|
|
42
|
+
let cachedCaptureScreen;
|
|
43
|
+
let cachedRecordScreenModule;
|
|
44
|
+
let cachedAudioModule;
|
|
45
|
+
function getDeviceInfoModule() {
|
|
46
|
+
if (cachedDeviceInfoModule !== undefined) {
|
|
47
|
+
return cachedDeviceInfoModule;
|
|
48
|
+
}
|
|
49
|
+
const required = loadRawModule('react-native-device-info');
|
|
50
|
+
cachedDeviceInfoModule = required ? getDefaultExport(required) : null;
|
|
51
|
+
return cachedDeviceInfoModule;
|
|
52
|
+
}
|
|
53
|
+
function getCaptureScreenFn() {
|
|
54
|
+
if (cachedCaptureScreen !== undefined) {
|
|
55
|
+
return cachedCaptureScreen;
|
|
56
|
+
}
|
|
57
|
+
const required = loadRawModule('react-native-view-shot');
|
|
58
|
+
cachedCaptureScreen = required?.captureScreen ?? null;
|
|
59
|
+
if (!cachedCaptureScreen) {
|
|
60
|
+
warnOnce('missing:react-native-view-shot:captureScreen', '[iInstall SDK] Screenshot capture unavailable: react-native-view-shot is not linked correctly.');
|
|
61
|
+
}
|
|
62
|
+
return cachedCaptureScreen;
|
|
63
|
+
}
|
|
64
|
+
function getRecordScreenModule() {
|
|
65
|
+
if (cachedRecordScreenModule !== undefined) {
|
|
66
|
+
return cachedRecordScreenModule;
|
|
67
|
+
}
|
|
68
|
+
const required = loadRawModule('react-native-record-screen');
|
|
69
|
+
cachedRecordScreenModule = required ? getDefaultExport(required) : null;
|
|
70
|
+
return cachedRecordScreenModule;
|
|
71
|
+
}
|
|
72
|
+
function getAudioRecorderModule() {
|
|
73
|
+
if (cachedAudioModule !== undefined) {
|
|
74
|
+
return cachedAudioModule;
|
|
75
|
+
}
|
|
76
|
+
cachedAudioModule = loadRawModule('react-native-audio-recorder-player');
|
|
77
|
+
return cachedAudioModule;
|
|
78
|
+
}
|
|
79
|
+
function fallbackString(value, defaultValue) {
|
|
80
|
+
if (!value || value.trim().length === 0) {
|
|
81
|
+
return defaultValue;
|
|
82
|
+
}
|
|
83
|
+
return value;
|
|
84
|
+
}
|
|
85
|
+
async function isEmulatorDevice() {
|
|
86
|
+
const moduleRef = getDeviceInfoModule();
|
|
87
|
+
if (!moduleRef?.isEmulator) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
return await moduleRef.isEmulator();
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async function getDeviceUniqueId() {
|
|
98
|
+
const moduleRef = getDeviceInfoModule();
|
|
99
|
+
if (!moduleRef?.getUniqueId) {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const value = await moduleRef.getUniqueId();
|
|
104
|
+
return fallbackString(value, 'unknown');
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function getDeviceMetadata() {
|
|
111
|
+
const moduleRef = getDeviceInfoModule();
|
|
112
|
+
const systemName = react_native_1.Platform.OS === 'ios' ? 'iOS' : react_native_1.Platform.OS;
|
|
113
|
+
const systemVersion = String(react_native_1.Platform.Version ?? 'unknown');
|
|
114
|
+
const metadata = {
|
|
115
|
+
device: 'Unknown Device',
|
|
116
|
+
systemName,
|
|
117
|
+
systemVersion,
|
|
118
|
+
appVersion: 'unknown',
|
|
119
|
+
buildNumber: 'unknown',
|
|
120
|
+
brand: react_native_1.Platform.OS === 'ios' ? 'Apple' : 'Unknown',
|
|
121
|
+
isEmulator: false,
|
|
122
|
+
};
|
|
123
|
+
if (!moduleRef) {
|
|
124
|
+
return metadata;
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
metadata.device = fallbackString(moduleRef.getModel?.(), metadata.device);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
// noop
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
metadata.systemName = fallbackString(moduleRef.getSystemName?.(), metadata.systemName);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// noop
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
metadata.systemVersion = fallbackString(moduleRef.getSystemVersion?.(), metadata.systemVersion);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// noop
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
metadata.appVersion = fallbackString(moduleRef.getVersion?.(), metadata.appVersion);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// noop
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
metadata.buildNumber = fallbackString(moduleRef.getBuildNumber?.(), metadata.buildNumber);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
// noop
|
|
155
|
+
}
|
|
156
|
+
try {
|
|
157
|
+
metadata.brand = fallbackString(moduleRef.getBrand?.(), metadata.brand);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
// noop
|
|
161
|
+
}
|
|
162
|
+
try {
|
|
163
|
+
metadata.isEmulator = moduleRef.isEmulator ? await moduleRef.isEmulator() : false;
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
metadata.isEmulator = false;
|
|
167
|
+
}
|
|
168
|
+
return metadata;
|
|
169
|
+
}
|
|
170
|
+
function hasScreenRecordingSupport() {
|
|
171
|
+
const moduleRef = getRecordScreenModule();
|
|
172
|
+
return Boolean(moduleRef?.startRecording && moduleRef?.stopRecording);
|
|
173
|
+
}
|
|
174
|
+
async function startScreenRecording(config = { mic: true }) {
|
|
175
|
+
const moduleRef = getRecordScreenModule();
|
|
176
|
+
if (!moduleRef?.startRecording) {
|
|
177
|
+
throw new Error('Screen recording dependency missing. Install react-native-record-screen and rebuild the app.');
|
|
178
|
+
}
|
|
179
|
+
await moduleRef.startRecording(config);
|
|
180
|
+
}
|
|
181
|
+
async function stopScreenRecording() {
|
|
182
|
+
const moduleRef = getRecordScreenModule();
|
|
183
|
+
if (!moduleRef?.stopRecording) {
|
|
184
|
+
throw new Error('Screen recording dependency missing. Install react-native-record-screen and rebuild the app.');
|
|
185
|
+
}
|
|
186
|
+
return moduleRef.stopRecording();
|
|
187
|
+
}
|
|
188
|
+
async function captureScreenImage(options) {
|
|
189
|
+
const captureScreen = getCaptureScreenFn();
|
|
190
|
+
if (!captureScreen) {
|
|
191
|
+
throw new Error('Screenshot dependency missing. Install react-native-view-shot and rebuild the app.');
|
|
192
|
+
}
|
|
193
|
+
return captureScreen(options);
|
|
194
|
+
}
|
|
195
|
+
function hasAudioRecordingSupport() {
|
|
196
|
+
const moduleRef = getAudioRecorderModule();
|
|
197
|
+
const recorderCtor = typeof moduleRef === 'function' ? moduleRef : moduleRef?.default;
|
|
198
|
+
return typeof recorderCtor === 'function';
|
|
199
|
+
}
|
|
200
|
+
function createAudioRecorderPlayer() {
|
|
201
|
+
const moduleRef = getAudioRecorderModule();
|
|
202
|
+
const recorderCtor = typeof moduleRef === 'function' ? moduleRef : moduleRef?.default;
|
|
203
|
+
if (typeof recorderCtor !== 'function') {
|
|
204
|
+
warnOnce('missing:react-native-audio-recorder-player:ctor', '[iInstall SDK] Audio recording unavailable: react-native-audio-recorder-player is not linked correctly.');
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
return new recorderCtor();
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
212
|
+
warnOnce('missing:react-native-audio-recorder-player:new', `[iInstall SDK] Failed to initialize audio recorder. (${reason})`);
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
function getAudioRecordingPreset() {
|
|
217
|
+
const moduleRef = getAudioRecorderModule();
|
|
218
|
+
const namedExports = typeof moduleRef === 'object' && moduleRef !== null ? moduleRef : undefined;
|
|
219
|
+
return {
|
|
220
|
+
AVFormatIDKeyIOS: namedExports?.AVEncodingOption?.aac ?? 'aac',
|
|
221
|
+
AVEncoderAudioQualityKeyIOS: namedExports?.AVEncoderAudioQualityIOSType?.high ?? 96,
|
|
222
|
+
AVSampleRateKeyIOS: 44100,
|
|
223
|
+
AVNumberOfChannelsKeyIOS: 1,
|
|
224
|
+
AVEncoderBitRateKeyIOS: 128000,
|
|
225
|
+
AudioEncoderAndroid: namedExports?.AudioEncoderAndroidType?.AAC ?? 3,
|
|
226
|
+
AudioSourceAndroid: namedExports?.AudioSourceAndroidType?.MIC ?? 1,
|
|
227
|
+
OutputFormatAndroid: namedExports?.OutputFormatAndroidType?.MPEG_4 ?? 2,
|
|
228
|
+
AudioEncodingBitRateAndroid: 128000,
|
|
229
|
+
AudioSamplingRateAndroid: 44100,
|
|
230
|
+
AudioChannelsAndroid: 1,
|
|
231
|
+
};
|
|
232
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-iinstall",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.15",
|
|
4
4
|
"description": "🎯 IInstall React Native SDK - The ultimate beta testing & QA feedback tool. Shake-to-report with voice recordings, screen recordings, and screenshots. Zero-config setup with TypeScript support. Perfect for beta testing, QA teams, and user feedback collection.",
|
|
5
5
|
"author": "TesterFlow Team",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,22 +34,28 @@
|
|
|
34
34
|
"files": [
|
|
35
35
|
"lib",
|
|
36
36
|
"src",
|
|
37
|
-
"INTEGRATION_GUIDE.md"
|
|
37
|
+
"INTEGRATION_GUIDE.md",
|
|
38
|
+
"RELEASE_MANIFEST.json"
|
|
38
39
|
],
|
|
39
40
|
"scripts": {
|
|
40
|
-
"
|
|
41
|
+
"prebuild": "npm run prepare:release",
|
|
42
|
+
"clean:build": "node -e \"require('fs').rmSync('lib',{ recursive: true, force: true })\"",
|
|
43
|
+
"build": "npm run clean:build && tsc",
|
|
44
|
+
"sync:release-manifest": "node scripts/sync-release-manifest.mjs",
|
|
45
|
+
"check:release-manifest": "node scripts/check-release-manifest.mjs",
|
|
46
|
+
"prepare:release": "npm run sync:release-manifest && npm run check:release-manifest",
|
|
41
47
|
"prepare": "npm run build"
|
|
42
48
|
},
|
|
43
49
|
"peerDependencies": {
|
|
44
50
|
"react": ">=16.8.0",
|
|
45
|
-
"react-native": ">=0.60.0"
|
|
46
|
-
},
|
|
47
|
-
"dependencies": {
|
|
48
|
-
"react-native-sensors": "^7.3.0",
|
|
49
|
-
"react-native-view-shot": "^3.1.2",
|
|
50
|
-
"react-native-device-info": "^10.0.0",
|
|
51
|
+
"react-native": ">=0.60.0",
|
|
51
52
|
"react-native-audio-recorder-player": "^3.6.4",
|
|
53
|
+
"react-native-device-info": "^10.0.0",
|
|
52
54
|
"react-native-record-screen": "^0.6.2",
|
|
55
|
+
"react-native-sensors": "^7.3.0",
|
|
56
|
+
"react-native-view-shot": "^3.1.2"
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
53
59
|
"rxjs": "^7.0.0"
|
|
54
60
|
},
|
|
55
61
|
"devDependencies": {
|
|
@@ -57,6 +63,11 @@
|
|
|
57
63
|
"@types/react-native": "^0.70.0",
|
|
58
64
|
"react": "18.2.0",
|
|
59
65
|
"react-native": "0.72.6",
|
|
66
|
+
"react-native-audio-recorder-player": "^3.6.4",
|
|
67
|
+
"react-native-device-info": "^10.0.0",
|
|
68
|
+
"react-native-record-screen": "^0.6.2",
|
|
69
|
+
"react-native-sensors": "^7.3.0",
|
|
70
|
+
"react-native-view-shot": "^3.1.2",
|
|
60
71
|
"typescript": "^5.0.0"
|
|
61
72
|
}
|
|
62
73
|
}
|
package/src/FeedbackModal.tsx
CHANGED
|
@@ -16,15 +16,13 @@ import {
|
|
|
16
16
|
TouchableWithoutFeedback,
|
|
17
17
|
ScrollView
|
|
18
18
|
} from 'react-native';
|
|
19
|
-
import
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
type AudioSet,
|
|
27
|
-
} from 'react-native-audio-recorder-player';
|
|
19
|
+
import {
|
|
20
|
+
createAudioRecorderPlayer,
|
|
21
|
+
getAudioRecordingPreset,
|
|
22
|
+
getDeviceMetadata,
|
|
23
|
+
getDeviceUniqueId,
|
|
24
|
+
hasAudioRecordingSupport,
|
|
25
|
+
} from './nativeModules';
|
|
28
26
|
|
|
29
27
|
interface FeedbackModalProps {
|
|
30
28
|
visible: boolean;
|
|
@@ -52,19 +50,28 @@ export const FeedbackModal: React.FC<FeedbackModalProps> = ({
|
|
|
52
50
|
const [isRecordingAudio, setIsRecordingAudio] = useState(false);
|
|
53
51
|
const [audioUri, setAudioUri] = useState<string | null>(null);
|
|
54
52
|
const [audioDuration, setAudioDuration] = useState('00:00');
|
|
53
|
+
const audioRecordingAvailable = hasAudioRecordingSupport();
|
|
55
54
|
|
|
56
|
-
const audioRecorderPlayer = useRef(
|
|
55
|
+
const audioRecorderPlayer = useRef(createAudioRecorderPlayer()).current;
|
|
57
56
|
|
|
58
57
|
useEffect(() => {
|
|
59
58
|
return () => {
|
|
60
59
|
// Cleanup
|
|
61
|
-
if (isRecordingAudio) {
|
|
62
|
-
audioRecorderPlayer.stopRecorder();
|
|
60
|
+
if (isRecordingAudio && audioRecorderPlayer) {
|
|
61
|
+
audioRecorderPlayer.stopRecorder().catch(() => undefined);
|
|
63
62
|
}
|
|
64
63
|
};
|
|
65
|
-
}, [isRecordingAudio]);
|
|
64
|
+
}, [audioRecorderPlayer, isRecordingAudio]);
|
|
66
65
|
|
|
67
66
|
const onStartAudioRecord = async () => {
|
|
67
|
+
if (!audioRecorderPlayer || !audioRecordingAvailable) {
|
|
68
|
+
Alert.alert(
|
|
69
|
+
'Audio unavailable',
|
|
70
|
+
'Install react-native-audio-recorder-player in your app root, then rebuild the app.'
|
|
71
|
+
);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
68
75
|
if (Platform.OS === 'android') {
|
|
69
76
|
try {
|
|
70
77
|
const grants = await PermissionsAndroid.requestMultiple([
|
|
@@ -86,23 +93,9 @@ export const FeedbackModal: React.FC<FeedbackModalProps> = ({
|
|
|
86
93
|
}
|
|
87
94
|
|
|
88
95
|
try {
|
|
89
|
-
const audioSet: AudioSet = {
|
|
90
|
-
AVFormatIDKeyIOS: AVEncodingOption.aac,
|
|
91
|
-
AVEncoderAudioQualityKeyIOS: AVEncoderAudioQualityIOSType.high,
|
|
92
|
-
AVSampleRateKeyIOS: 44100,
|
|
93
|
-
AVNumberOfChannelsKeyIOS: 1,
|
|
94
|
-
AVEncoderBitRateKeyIOS: 128000,
|
|
95
|
-
AudioEncoderAndroid: AudioEncoderAndroidType.AAC,
|
|
96
|
-
AudioSourceAndroid: AudioSourceAndroidType.MIC,
|
|
97
|
-
OutputFormatAndroid: OutputFormatAndroidType.MPEG_4,
|
|
98
|
-
AudioEncodingBitRateAndroid: 128000,
|
|
99
|
-
AudioSamplingRateAndroid: 44100,
|
|
100
|
-
AudioChannelsAndroid: 1,
|
|
101
|
-
};
|
|
102
|
-
|
|
103
96
|
const result = await audioRecorderPlayer.startRecorder(
|
|
104
97
|
undefined,
|
|
105
|
-
|
|
98
|
+
getAudioRecordingPreset(),
|
|
106
99
|
);
|
|
107
100
|
audioRecorderPlayer.addRecordBackListener((e) => {
|
|
108
101
|
setAudioDuration(audioRecorderPlayer.mmssss(Math.floor(e.currentPosition)));
|
|
@@ -117,6 +110,10 @@ export const FeedbackModal: React.FC<FeedbackModalProps> = ({
|
|
|
117
110
|
};
|
|
118
111
|
|
|
119
112
|
const onStopAudioRecord = async () => {
|
|
113
|
+
if (!audioRecorderPlayer) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
120
117
|
try {
|
|
121
118
|
const result = await audioRecorderPlayer.stopRecorder();
|
|
122
119
|
audioRecorderPlayer.removeRecordBackListener();
|
|
@@ -134,7 +131,7 @@ export const FeedbackModal: React.FC<FeedbackModalProps> = ({
|
|
|
134
131
|
};
|
|
135
132
|
|
|
136
133
|
const handleClose = async () => {
|
|
137
|
-
if (isRecordingAudio) {
|
|
134
|
+
if (isRecordingAudio && audioRecorderPlayer) {
|
|
138
135
|
try {
|
|
139
136
|
await audioRecorderPlayer.stopRecorder();
|
|
140
137
|
} catch (error) {
|
|
@@ -203,21 +200,14 @@ export const FeedbackModal: React.FC<FeedbackModalProps> = ({
|
|
|
203
200
|
}
|
|
204
201
|
|
|
205
202
|
// 4. Metadata
|
|
206
|
-
const metadata =
|
|
207
|
-
device: DeviceInfo.getModel(),
|
|
208
|
-
systemName: DeviceInfo.getSystemName(),
|
|
209
|
-
systemVersion: DeviceInfo.getSystemVersion(),
|
|
210
|
-
appVersion: DeviceInfo.getVersion(),
|
|
211
|
-
buildNumber: DeviceInfo.getBuildNumber(),
|
|
212
|
-
brand: DeviceInfo.getBrand(),
|
|
213
|
-
isEmulator: await DeviceInfo.isEmulator(),
|
|
214
|
-
};
|
|
203
|
+
const metadata = await getDeviceMetadata();
|
|
215
204
|
formData.append('metadata', JSON.stringify(metadata));
|
|
216
205
|
|
|
217
206
|
// 5. Other fields
|
|
218
207
|
formData.append('description', description);
|
|
219
208
|
formData.append('apiKey', apiKey);
|
|
220
|
-
|
|
209
|
+
const deviceUdid = await getDeviceUniqueId();
|
|
210
|
+
formData.append('udid', deviceUdid || 'unknown');
|
|
221
211
|
|
|
222
212
|
// 6. Send
|
|
223
213
|
const response = await fetch(`${apiEndpoint}/api/feedback`, {
|
|
@@ -306,11 +296,23 @@ export const FeedbackModal: React.FC<FeedbackModalProps> = ({
|
|
|
306
296
|
<View style={styles.actionsRow}>
|
|
307
297
|
{/* Audio Recorder */}
|
|
308
298
|
<TouchableOpacity
|
|
309
|
-
style={[
|
|
299
|
+
style={[
|
|
300
|
+
styles.actionBtn,
|
|
301
|
+
isRecordingAudio ? styles.recordingBtn : undefined,
|
|
302
|
+
audioUri ? styles.hasAudioBtn : undefined,
|
|
303
|
+
!audioRecordingAvailable ? styles.disabledActionBtn : undefined,
|
|
304
|
+
]}
|
|
310
305
|
onPress={isRecordingAudio ? onStopAudioRecord : onStartAudioRecord}
|
|
306
|
+
disabled={!audioRecordingAvailable}
|
|
311
307
|
>
|
|
312
308
|
<Text style={[styles.actionBtnText, (isRecordingAudio || audioUri) ? { color: '#FFF' } : undefined]}>
|
|
313
|
-
{
|
|
309
|
+
{!audioRecordingAvailable
|
|
310
|
+
? '🎙 Audio Unavailable'
|
|
311
|
+
: isRecordingAudio
|
|
312
|
+
? `Stop (${audioDuration})`
|
|
313
|
+
: audioUri
|
|
314
|
+
? 'Re-record Audio'
|
|
315
|
+
: '🎙 Record Audio'}
|
|
314
316
|
</Text>
|
|
315
317
|
</TouchableOpacity>
|
|
316
318
|
|
|
@@ -437,6 +439,9 @@ const styles = StyleSheet.create({
|
|
|
437
439
|
hasAudioBtn: {
|
|
438
440
|
backgroundColor: '#4CAF50',
|
|
439
441
|
},
|
|
442
|
+
disabledActionBtn: {
|
|
443
|
+
opacity: 0.45,
|
|
444
|
+
},
|
|
440
445
|
actionBtnText: {
|
|
441
446
|
fontSize: 14,
|
|
442
447
|
fontWeight: '600',
|
package/src/index.tsx
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import React, { useEffect, useState, useRef } from 'react';
|
|
2
2
|
import { View, StyleSheet, TouchableOpacity, Text, SafeAreaView, Platform } from 'react-native';
|
|
3
|
-
import { captureScreen } from 'react-native-view-shot';
|
|
4
3
|
import { ShakeDetector } from './ShakeDetector';
|
|
5
4
|
import { FeedbackModal } from './FeedbackModal';
|
|
6
|
-
import RecordScreen from 'react-native-record-screen';
|
|
7
|
-
import DeviceInfo from 'react-native-device-info';
|
|
8
5
|
import { registerPushToken } from './pushRegistration';
|
|
6
|
+
import {
|
|
7
|
+
captureScreenImage,
|
|
8
|
+
getDeviceUniqueId,
|
|
9
|
+
hasScreenRecordingSupport,
|
|
10
|
+
isEmulatorDevice,
|
|
11
|
+
startScreenRecording,
|
|
12
|
+
stopScreenRecording,
|
|
13
|
+
} from './nativeModules';
|
|
9
14
|
|
|
10
15
|
interface IInstallProps {
|
|
11
16
|
apiKey: string;
|
|
@@ -53,7 +58,7 @@ export const IInstall: React.FC<IInstallProps> = ({
|
|
|
53
58
|
useEffect(() => {
|
|
54
59
|
let mounted = true;
|
|
55
60
|
|
|
56
|
-
|
|
61
|
+
isEmulatorDevice()
|
|
57
62
|
.then((value) => {
|
|
58
63
|
if (mounted) {
|
|
59
64
|
setIsEmulator(value);
|
|
@@ -75,7 +80,7 @@ export const IInstall: React.FC<IInstallProps> = ({
|
|
|
75
80
|
|
|
76
81
|
try {
|
|
77
82
|
// Capture screenshot
|
|
78
|
-
const uri = await
|
|
83
|
+
const uri = await captureScreenImage({
|
|
79
84
|
format: 'png',
|
|
80
85
|
quality: 0.8,
|
|
81
86
|
});
|
|
@@ -113,7 +118,7 @@ export const IInstall: React.FC<IInstallProps> = ({
|
|
|
113
118
|
let cancelled = false;
|
|
114
119
|
|
|
115
120
|
const syncPushToken = async () => {
|
|
116
|
-
const deviceUdid = await
|
|
121
|
+
const deviceUdid = await getDeviceUniqueId();
|
|
117
122
|
const result = await registerPushToken({
|
|
118
123
|
token: pushToken,
|
|
119
124
|
apiKey,
|
|
@@ -168,6 +173,7 @@ export const IInstall: React.FC<IInstallProps> = ({
|
|
|
168
173
|
showFloatingButtonOnEmulator &&
|
|
169
174
|
!modalVisible &&
|
|
170
175
|
!isRecording;
|
|
176
|
+
const screenRecordingAvailable = hasScreenRecordingSupport();
|
|
171
177
|
|
|
172
178
|
const normalizeVideoUri = (value: string) => {
|
|
173
179
|
if (Platform.OS === 'ios' && value.startsWith('/')) {
|
|
@@ -242,10 +248,18 @@ export const IInstall: React.FC<IInstallProps> = ({
|
|
|
242
248
|
};
|
|
243
249
|
|
|
244
250
|
const handleStartRecording = async () => {
|
|
251
|
+
if (!screenRecordingAvailable) {
|
|
252
|
+
console.warn(
|
|
253
|
+
'IInstall: Screen recording unavailable. Install react-native-record-screen in your app and rebuild.'
|
|
254
|
+
);
|
|
255
|
+
setModalVisible(true);
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
245
259
|
setModalVisible(false);
|
|
246
260
|
setIsRecording(true);
|
|
247
261
|
try {
|
|
248
|
-
await
|
|
262
|
+
await startScreenRecording({ mic: true });
|
|
249
263
|
} catch (e) {
|
|
250
264
|
console.error('Failed to start recording', e);
|
|
251
265
|
setIsRecording(false);
|
|
@@ -256,7 +270,7 @@ export const IInstall: React.FC<IInstallProps> = ({
|
|
|
256
270
|
const handleStopRecording = async () => {
|
|
257
271
|
try {
|
|
258
272
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
259
|
-
const res: any = await
|
|
273
|
+
const res: any = await stopScreenRecording();
|
|
260
274
|
if (res) {
|
|
261
275
|
const nextVideoUri = resolveRecordedVideoUri(res);
|
|
262
276
|
if (nextVideoUri) {
|
|
@@ -315,7 +329,7 @@ export const IInstall: React.FC<IInstallProps> = ({
|
|
|
315
329
|
}}
|
|
316
330
|
screenshotUri={screenshotUri}
|
|
317
331
|
videoUri={videoUri}
|
|
318
|
-
onStartRecording={handleStartRecording}
|
|
332
|
+
onStartRecording={screenRecordingAvailable ? handleStartRecording : undefined}
|
|
319
333
|
apiKey={apiKey}
|
|
320
334
|
apiEndpoint={apiEndpoint}
|
|
321
335
|
/>
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { Platform } from 'react-native';
|
|
2
|
+
|
|
3
|
+
type CaptureScreenOptions = {
|
|
4
|
+
format?: 'png' | 'jpg' | 'webm' | 'raw';
|
|
5
|
+
quality?: number;
|
|
6
|
+
result?: 'tmpfile' | 'base64' | 'data-uri' | 'zip-base64';
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
type DeviceInfoModule = {
|
|
10
|
+
getModel?: () => string;
|
|
11
|
+
getSystemName?: () => string;
|
|
12
|
+
getSystemVersion?: () => string;
|
|
13
|
+
getVersion?: () => string;
|
|
14
|
+
getBuildNumber?: () => string;
|
|
15
|
+
getBrand?: () => string;
|
|
16
|
+
isEmulator?: () => Promise<boolean>;
|
|
17
|
+
getUniqueId?: () => Promise<string>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type ViewShotModule = {
|
|
21
|
+
captureScreen?: (options?: CaptureScreenOptions) => Promise<string>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type RecordScreenModule = {
|
|
25
|
+
startRecording?: (config?: { mic?: boolean; fps?: number; bitrate?: number }) => Promise<unknown>;
|
|
26
|
+
stopRecording?: () => Promise<unknown>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type AudioRecorderModule = {
|
|
30
|
+
default?: new () => AudioRecorderPlayerLike;
|
|
31
|
+
AVEncodingOption?: { aac?: string };
|
|
32
|
+
AVEncoderAudioQualityIOSType?: { high?: number };
|
|
33
|
+
AudioEncoderAndroidType?: { AAC?: number };
|
|
34
|
+
AudioSourceAndroidType?: { MIC?: number };
|
|
35
|
+
OutputFormatAndroidType?: { MPEG_4?: number };
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type AudioRecorderModuleValue = AudioRecorderModule | (new () => AudioRecorderPlayerLike);
|
|
39
|
+
|
|
40
|
+
type RecordBackEvent = {
|
|
41
|
+
currentPosition: number;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type AudioRecorderPlayerLike = {
|
|
45
|
+
startRecorder: (uri?: string, audioSets?: Record<string, unknown>) => Promise<string>;
|
|
46
|
+
stopRecorder: () => Promise<string>;
|
|
47
|
+
addRecordBackListener: (callback: (recordingMeta: RecordBackEvent) => void) => void;
|
|
48
|
+
removeRecordBackListener: () => void;
|
|
49
|
+
mmssss: (milisecs: number) => string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type DeviceMetadata = {
|
|
53
|
+
device: string;
|
|
54
|
+
systemName: string;
|
|
55
|
+
systemVersion: string;
|
|
56
|
+
appVersion: string;
|
|
57
|
+
buildNumber: string;
|
|
58
|
+
brand: string;
|
|
59
|
+
isEmulator: boolean;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const warnedKeys = new Set<string>();
|
|
63
|
+
|
|
64
|
+
function warnOnce(key: string, message: string) {
|
|
65
|
+
if (warnedKeys.has(key)) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
warnedKeys.add(key);
|
|
69
|
+
console.warn(message);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function getDefaultExport<T>(moduleValue: unknown): T | null {
|
|
73
|
+
if (!moduleValue) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
if (typeof moduleValue === 'object' && moduleValue !== null && 'default' in moduleValue) {
|
|
77
|
+
return (moduleValue as { default: T }).default;
|
|
78
|
+
}
|
|
79
|
+
return moduleValue as T;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function loadRawModule<T>(moduleName: string): T | null {
|
|
83
|
+
try {
|
|
84
|
+
return require(moduleName) as T;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
87
|
+
warnOnce(
|
|
88
|
+
`missing:${moduleName}`,
|
|
89
|
+
`[iInstall SDK] Missing native dependency "${moduleName}". Install it in your app root and rebuild. (${reason})`
|
|
90
|
+
);
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let cachedDeviceInfoModule: DeviceInfoModule | null | undefined;
|
|
96
|
+
let cachedCaptureScreen: ((options?: CaptureScreenOptions) => Promise<string>) | null | undefined;
|
|
97
|
+
let cachedRecordScreenModule: RecordScreenModule | null | undefined;
|
|
98
|
+
let cachedAudioModule: AudioRecorderModuleValue | null | undefined;
|
|
99
|
+
|
|
100
|
+
function getDeviceInfoModule(): DeviceInfoModule | null {
|
|
101
|
+
if (cachedDeviceInfoModule !== undefined) {
|
|
102
|
+
return cachedDeviceInfoModule;
|
|
103
|
+
}
|
|
104
|
+
const required = loadRawModule<unknown>('react-native-device-info');
|
|
105
|
+
cachedDeviceInfoModule = required ? getDefaultExport<DeviceInfoModule>(required) : null;
|
|
106
|
+
return cachedDeviceInfoModule;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function getCaptureScreenFn(): ((options?: CaptureScreenOptions) => Promise<string>) | null {
|
|
110
|
+
if (cachedCaptureScreen !== undefined) {
|
|
111
|
+
return cachedCaptureScreen;
|
|
112
|
+
}
|
|
113
|
+
const required = loadRawModule<ViewShotModule>('react-native-view-shot');
|
|
114
|
+
cachedCaptureScreen = required?.captureScreen ?? null;
|
|
115
|
+
if (!cachedCaptureScreen) {
|
|
116
|
+
warnOnce(
|
|
117
|
+
'missing:react-native-view-shot:captureScreen',
|
|
118
|
+
'[iInstall SDK] Screenshot capture unavailable: react-native-view-shot is not linked correctly.'
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return cachedCaptureScreen;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function getRecordScreenModule(): RecordScreenModule | null {
|
|
125
|
+
if (cachedRecordScreenModule !== undefined) {
|
|
126
|
+
return cachedRecordScreenModule;
|
|
127
|
+
}
|
|
128
|
+
const required = loadRawModule<unknown>('react-native-record-screen');
|
|
129
|
+
cachedRecordScreenModule = required ? getDefaultExport<RecordScreenModule>(required) : null;
|
|
130
|
+
return cachedRecordScreenModule;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function getAudioRecorderModule(): AudioRecorderModuleValue | null {
|
|
134
|
+
if (cachedAudioModule !== undefined) {
|
|
135
|
+
return cachedAudioModule;
|
|
136
|
+
}
|
|
137
|
+
cachedAudioModule = loadRawModule<AudioRecorderModuleValue>('react-native-audio-recorder-player');
|
|
138
|
+
return cachedAudioModule;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function fallbackString(value: string | undefined, defaultValue: string): string {
|
|
142
|
+
if (!value || value.trim().length === 0) {
|
|
143
|
+
return defaultValue;
|
|
144
|
+
}
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function isEmulatorDevice(): Promise<boolean> {
|
|
149
|
+
const moduleRef = getDeviceInfoModule();
|
|
150
|
+
if (!moduleRef?.isEmulator) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
return await moduleRef.isEmulator();
|
|
155
|
+
} catch {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function getDeviceUniqueId(): Promise<string | undefined> {
|
|
161
|
+
const moduleRef = getDeviceInfoModule();
|
|
162
|
+
if (!moduleRef?.getUniqueId) {
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
const value = await moduleRef.getUniqueId();
|
|
167
|
+
return fallbackString(value, 'unknown');
|
|
168
|
+
} catch {
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function getDeviceMetadata(): Promise<DeviceMetadata> {
|
|
174
|
+
const moduleRef = getDeviceInfoModule();
|
|
175
|
+
const systemName = Platform.OS === 'ios' ? 'iOS' : Platform.OS;
|
|
176
|
+
const systemVersion = String(Platform.Version ?? 'unknown');
|
|
177
|
+
|
|
178
|
+
const metadata: DeviceMetadata = {
|
|
179
|
+
device: 'Unknown Device',
|
|
180
|
+
systemName,
|
|
181
|
+
systemVersion,
|
|
182
|
+
appVersion: 'unknown',
|
|
183
|
+
buildNumber: 'unknown',
|
|
184
|
+
brand: Platform.OS === 'ios' ? 'Apple' : 'Unknown',
|
|
185
|
+
isEmulator: false,
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
if (!moduleRef) {
|
|
189
|
+
return metadata;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
metadata.device = fallbackString(moduleRef.getModel?.(), metadata.device);
|
|
194
|
+
} catch {
|
|
195
|
+
// noop
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
metadata.systemName = fallbackString(moduleRef.getSystemName?.(), metadata.systemName);
|
|
199
|
+
} catch {
|
|
200
|
+
// noop
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
metadata.systemVersion = fallbackString(moduleRef.getSystemVersion?.(), metadata.systemVersion);
|
|
204
|
+
} catch {
|
|
205
|
+
// noop
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
metadata.appVersion = fallbackString(moduleRef.getVersion?.(), metadata.appVersion);
|
|
209
|
+
} catch {
|
|
210
|
+
// noop
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
metadata.buildNumber = fallbackString(moduleRef.getBuildNumber?.(), metadata.buildNumber);
|
|
214
|
+
} catch {
|
|
215
|
+
// noop
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
metadata.brand = fallbackString(moduleRef.getBrand?.(), metadata.brand);
|
|
219
|
+
} catch {
|
|
220
|
+
// noop
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
metadata.isEmulator = moduleRef.isEmulator ? await moduleRef.isEmulator() : false;
|
|
224
|
+
} catch {
|
|
225
|
+
metadata.isEmulator = false;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return metadata;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function hasScreenRecordingSupport(): boolean {
|
|
232
|
+
const moduleRef = getRecordScreenModule();
|
|
233
|
+
return Boolean(moduleRef?.startRecording && moduleRef?.stopRecording);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export async function startScreenRecording(config: { mic?: boolean } = { mic: true }): Promise<void> {
|
|
237
|
+
const moduleRef = getRecordScreenModule();
|
|
238
|
+
if (!moduleRef?.startRecording) {
|
|
239
|
+
throw new Error(
|
|
240
|
+
'Screen recording dependency missing. Install react-native-record-screen and rebuild the app.'
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
await moduleRef.startRecording(config);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export async function stopScreenRecording(): Promise<unknown> {
|
|
247
|
+
const moduleRef = getRecordScreenModule();
|
|
248
|
+
if (!moduleRef?.stopRecording) {
|
|
249
|
+
throw new Error(
|
|
250
|
+
'Screen recording dependency missing. Install react-native-record-screen and rebuild the app.'
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
return moduleRef.stopRecording();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export async function captureScreenImage(options?: CaptureScreenOptions): Promise<string> {
|
|
257
|
+
const captureScreen = getCaptureScreenFn();
|
|
258
|
+
if (!captureScreen) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
'Screenshot dependency missing. Install react-native-view-shot and rebuild the app.'
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
return captureScreen(options);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function hasAudioRecordingSupport(): boolean {
|
|
267
|
+
const moduleRef = getAudioRecorderModule();
|
|
268
|
+
const recorderCtor =
|
|
269
|
+
typeof moduleRef === 'function' ? moduleRef : moduleRef?.default;
|
|
270
|
+
return typeof recorderCtor === 'function';
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function createAudioRecorderPlayer(): AudioRecorderPlayerLike | null {
|
|
274
|
+
const moduleRef = getAudioRecorderModule();
|
|
275
|
+
const recorderCtor =
|
|
276
|
+
typeof moduleRef === 'function' ? moduleRef : moduleRef?.default;
|
|
277
|
+
if (typeof recorderCtor !== 'function') {
|
|
278
|
+
warnOnce(
|
|
279
|
+
'missing:react-native-audio-recorder-player:ctor',
|
|
280
|
+
'[iInstall SDK] Audio recording unavailable: react-native-audio-recorder-player is not linked correctly.'
|
|
281
|
+
);
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
return new recorderCtor();
|
|
287
|
+
} catch (error) {
|
|
288
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
289
|
+
warnOnce(
|
|
290
|
+
'missing:react-native-audio-recorder-player:new',
|
|
291
|
+
`[iInstall SDK] Failed to initialize audio recorder. (${reason})`
|
|
292
|
+
);
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function getAudioRecordingPreset(): Record<string, string | number> {
|
|
298
|
+
const moduleRef = getAudioRecorderModule();
|
|
299
|
+
const namedExports = typeof moduleRef === 'object' && moduleRef !== null ? moduleRef : undefined;
|
|
300
|
+
return {
|
|
301
|
+
AVFormatIDKeyIOS: namedExports?.AVEncodingOption?.aac ?? 'aac',
|
|
302
|
+
AVEncoderAudioQualityKeyIOS: namedExports?.AVEncoderAudioQualityIOSType?.high ?? 96,
|
|
303
|
+
AVSampleRateKeyIOS: 44100,
|
|
304
|
+
AVNumberOfChannelsKeyIOS: 1,
|
|
305
|
+
AVEncoderBitRateKeyIOS: 128000,
|
|
306
|
+
AudioEncoderAndroid: namedExports?.AudioEncoderAndroidType?.AAC ?? 3,
|
|
307
|
+
AudioSourceAndroid: namedExports?.AudioSourceAndroidType?.MIC ?? 1,
|
|
308
|
+
OutputFormatAndroid: namedExports?.OutputFormatAndroidType?.MPEG_4 ?? 2,
|
|
309
|
+
AudioEncodingBitRateAndroid: 128000,
|
|
310
|
+
AudioSamplingRateAndroid: 44100,
|
|
311
|
+
AudioChannelsAndroid: 1,
|
|
312
|
+
};
|
|
313
|
+
}
|