react-native-iinstall 0.2.13 → 0.2.14
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 +6 -2
- package/RELEASE_MANIFEST.json +8 -0
- package/lib/scripts/check-release-manifest.d.mts +2 -0
- package/lib/scripts/check-release-manifest.mjs +38 -0
- package/lib/scripts/sync-release-manifest.d.mts +2 -0
- package/lib/scripts/sync-release-manifest.mjs +38 -0
- package/lib/src/FeedbackModal.d.ts +12 -0
- package/lib/src/FeedbackModal.js +393 -0
- package/lib/src/IInstallWrapper.d.ts +16 -0
- package/lib/src/IInstallWrapper.js +18 -0
- package/lib/src/ShakeDetector.d.ts +8 -0
- package/lib/src/ShakeDetector.js +68 -0
- package/lib/src/index.d.ts +17 -0
- package/lib/src/index.js +349 -0
- package/lib/src/nativeModules.d.ts +37 -0
- package/lib/src/nativeModules.js +231 -0
- package/lib/src/pushRegistration.d.ts +22 -0
- package/lib/src/pushRegistration.js +70 -0
- package/package.json +18 -8
- package/src/FeedbackModal.tsx +46 -41
- package/src/index.tsx +23 -9
- package/src/nativeModules.ts +308 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerPushToken = registerPushToken;
|
|
4
|
+
exports.unregisterPushToken = unregisterPushToken;
|
|
5
|
+
const react_native_1 = require("react-native");
|
|
6
|
+
function normalizeApiEndpoint(apiEndpoint) {
|
|
7
|
+
return apiEndpoint.replace(/\/+$/, '');
|
|
8
|
+
}
|
|
9
|
+
function resolvePlatform(platform) {
|
|
10
|
+
if (platform)
|
|
11
|
+
return platform;
|
|
12
|
+
return react_native_1.Platform.OS === 'ios' ? 'IOS' : 'ANDROID';
|
|
13
|
+
}
|
|
14
|
+
async function registerPushToken({ token, apiKey, apiEndpoint = 'https://iinstall.app', deviceUdid, projectId, platform, }) {
|
|
15
|
+
try {
|
|
16
|
+
const response = await fetch(`${normalizeApiEndpoint(apiEndpoint)}/api/notifications/push/register`, {
|
|
17
|
+
method: 'POST',
|
|
18
|
+
headers: { 'Content-Type': 'application/json' },
|
|
19
|
+
body: JSON.stringify({
|
|
20
|
+
token,
|
|
21
|
+
apiKey,
|
|
22
|
+
deviceUdid,
|
|
23
|
+
projectId,
|
|
24
|
+
platform: resolvePlatform(platform),
|
|
25
|
+
}),
|
|
26
|
+
});
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
28
|
+
const data = await response.json().catch(() => ({}));
|
|
29
|
+
return {
|
|
30
|
+
success: response.ok,
|
|
31
|
+
status: response.status,
|
|
32
|
+
data,
|
|
33
|
+
error: response.ok ? undefined : data?.error || 'Failed to register push token',
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
return {
|
|
38
|
+
success: false,
|
|
39
|
+
status: 500,
|
|
40
|
+
error: error instanceof Error ? error.message : 'Unknown register push error',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function unregisterPushToken({ token, apiKey, apiEndpoint = 'https://iinstall.app', }) {
|
|
45
|
+
try {
|
|
46
|
+
const response = await fetch(`${normalizeApiEndpoint(apiEndpoint)}/api/notifications/push/register`, {
|
|
47
|
+
method: 'DELETE',
|
|
48
|
+
headers: { 'Content-Type': 'application/json' },
|
|
49
|
+
body: JSON.stringify({
|
|
50
|
+
token,
|
|
51
|
+
apiKey,
|
|
52
|
+
}),
|
|
53
|
+
});
|
|
54
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
55
|
+
const data = await response.json().catch(() => ({}));
|
|
56
|
+
return {
|
|
57
|
+
success: response.ok,
|
|
58
|
+
status: response.status,
|
|
59
|
+
data,
|
|
60
|
+
error: response.ok ? undefined : data?.error || 'Failed to unregister push token',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
return {
|
|
65
|
+
success: false,
|
|
66
|
+
status: 500,
|
|
67
|
+
error: error instanceof Error ? error.message : 'Unknown unregister push error',
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-iinstall",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.14",
|
|
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,27 @@
|
|
|
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": {
|
|
41
|
+
"prebuild": "npm run prepare:release",
|
|
40
42
|
"build": "tsc",
|
|
43
|
+
"sync:release-manifest": "node scripts/sync-release-manifest.mjs",
|
|
44
|
+
"check:release-manifest": "node scripts/check-release-manifest.mjs",
|
|
45
|
+
"prepare:release": "npm run sync:release-manifest && npm run check:release-manifest",
|
|
41
46
|
"prepare": "npm run build"
|
|
42
47
|
},
|
|
43
48
|
"peerDependencies": {
|
|
44
49
|
"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",
|
|
50
|
+
"react-native": ">=0.60.0",
|
|
51
51
|
"react-native-audio-recorder-player": "^3.6.4",
|
|
52
|
+
"react-native-device-info": "^10.0.0",
|
|
52
53
|
"react-native-record-screen": "^0.6.2",
|
|
54
|
+
"react-native-sensors": "^7.3.0",
|
|
55
|
+
"react-native-view-shot": "^3.1.2"
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
53
58
|
"rxjs": "^7.0.0"
|
|
54
59
|
},
|
|
55
60
|
"devDependencies": {
|
|
@@ -57,6 +62,11 @@
|
|
|
57
62
|
"@types/react-native": "^0.70.0",
|
|
58
63
|
"react": "18.2.0",
|
|
59
64
|
"react-native": "0.72.6",
|
|
65
|
+
"react-native-audio-recorder-player": "^3.6.4",
|
|
66
|
+
"react-native-device-info": "^10.0.0",
|
|
67
|
+
"react-native-record-screen": "^0.6.2",
|
|
68
|
+
"react-native-sensors": "^7.3.0",
|
|
69
|
+
"react-native-view-shot": "^3.1.2",
|
|
60
70
|
"typescript": "^5.0.0"
|
|
61
71
|
}
|
|
62
72
|
}
|
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,308 @@
|
|
|
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 RecordBackEvent = {
|
|
39
|
+
currentPosition: number;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type AudioRecorderPlayerLike = {
|
|
43
|
+
startRecorder: (uri?: string, audioSets?: Record<string, unknown>) => Promise<string>;
|
|
44
|
+
stopRecorder: () => Promise<string>;
|
|
45
|
+
addRecordBackListener: (callback: (recordingMeta: RecordBackEvent) => void) => void;
|
|
46
|
+
removeRecordBackListener: () => void;
|
|
47
|
+
mmssss: (milisecs: number) => string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type DeviceMetadata = {
|
|
51
|
+
device: string;
|
|
52
|
+
systemName: string;
|
|
53
|
+
systemVersion: string;
|
|
54
|
+
appVersion: string;
|
|
55
|
+
buildNumber: string;
|
|
56
|
+
brand: string;
|
|
57
|
+
isEmulator: boolean;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const warnedKeys = new Set<string>();
|
|
61
|
+
|
|
62
|
+
function warnOnce(key: string, message: string) {
|
|
63
|
+
if (warnedKeys.has(key)) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
warnedKeys.add(key);
|
|
67
|
+
console.warn(message);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getDefaultExport<T>(moduleValue: unknown): T | null {
|
|
71
|
+
if (!moduleValue) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
if (typeof moduleValue === 'object' && moduleValue !== null && 'default' in moduleValue) {
|
|
75
|
+
return (moduleValue as { default: T }).default;
|
|
76
|
+
}
|
|
77
|
+
return moduleValue as T;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function loadRawModule<T>(moduleName: string): T | null {
|
|
81
|
+
try {
|
|
82
|
+
return require(moduleName) as T;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
85
|
+
warnOnce(
|
|
86
|
+
`missing:${moduleName}`,
|
|
87
|
+
`[iInstall SDK] Missing native dependency "${moduleName}". Install it in your app root and rebuild. (${reason})`
|
|
88
|
+
);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let cachedDeviceInfoModule: DeviceInfoModule | null | undefined;
|
|
94
|
+
let cachedCaptureScreen: ((options?: CaptureScreenOptions) => Promise<string>) | null | undefined;
|
|
95
|
+
let cachedRecordScreenModule: RecordScreenModule | null | undefined;
|
|
96
|
+
let cachedAudioModule: AudioRecorderModule | null | undefined;
|
|
97
|
+
|
|
98
|
+
function getDeviceInfoModule(): DeviceInfoModule | null {
|
|
99
|
+
if (cachedDeviceInfoModule !== undefined) {
|
|
100
|
+
return cachedDeviceInfoModule;
|
|
101
|
+
}
|
|
102
|
+
const required = loadRawModule<unknown>('react-native-device-info');
|
|
103
|
+
cachedDeviceInfoModule = required ? getDefaultExport<DeviceInfoModule>(required) : null;
|
|
104
|
+
return cachedDeviceInfoModule;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function getCaptureScreenFn(): ((options?: CaptureScreenOptions) => Promise<string>) | null {
|
|
108
|
+
if (cachedCaptureScreen !== undefined) {
|
|
109
|
+
return cachedCaptureScreen;
|
|
110
|
+
}
|
|
111
|
+
const required = loadRawModule<ViewShotModule>('react-native-view-shot');
|
|
112
|
+
cachedCaptureScreen = required?.captureScreen ?? null;
|
|
113
|
+
if (!cachedCaptureScreen) {
|
|
114
|
+
warnOnce(
|
|
115
|
+
'missing:react-native-view-shot:captureScreen',
|
|
116
|
+
'[iInstall SDK] Screenshot capture unavailable: react-native-view-shot is not linked correctly.'
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return cachedCaptureScreen;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function getRecordScreenModule(): RecordScreenModule | null {
|
|
123
|
+
if (cachedRecordScreenModule !== undefined) {
|
|
124
|
+
return cachedRecordScreenModule;
|
|
125
|
+
}
|
|
126
|
+
const required = loadRawModule<unknown>('react-native-record-screen');
|
|
127
|
+
cachedRecordScreenModule = required ? getDefaultExport<RecordScreenModule>(required) : null;
|
|
128
|
+
return cachedRecordScreenModule;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function getAudioRecorderModule(): AudioRecorderModule | null {
|
|
132
|
+
if (cachedAudioModule !== undefined) {
|
|
133
|
+
return cachedAudioModule;
|
|
134
|
+
}
|
|
135
|
+
cachedAudioModule = loadRawModule<AudioRecorderModule>('react-native-audio-recorder-player');
|
|
136
|
+
return cachedAudioModule;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function fallbackString(value: string | undefined, defaultValue: string): string {
|
|
140
|
+
if (!value || value.trim().length === 0) {
|
|
141
|
+
return defaultValue;
|
|
142
|
+
}
|
|
143
|
+
return value;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export async function isEmulatorDevice(): Promise<boolean> {
|
|
147
|
+
const moduleRef = getDeviceInfoModule();
|
|
148
|
+
if (!moduleRef?.isEmulator) {
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
return await moduleRef.isEmulator();
|
|
153
|
+
} catch {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export async function getDeviceUniqueId(): Promise<string | undefined> {
|
|
159
|
+
const moduleRef = getDeviceInfoModule();
|
|
160
|
+
if (!moduleRef?.getUniqueId) {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
const value = await moduleRef.getUniqueId();
|
|
165
|
+
return fallbackString(value, 'unknown');
|
|
166
|
+
} catch {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function getDeviceMetadata(): Promise<DeviceMetadata> {
|
|
172
|
+
const moduleRef = getDeviceInfoModule();
|
|
173
|
+
const systemName = Platform.OS === 'ios' ? 'iOS' : Platform.OS;
|
|
174
|
+
const systemVersion = String(Platform.Version ?? 'unknown');
|
|
175
|
+
|
|
176
|
+
const metadata: DeviceMetadata = {
|
|
177
|
+
device: 'Unknown Device',
|
|
178
|
+
systemName,
|
|
179
|
+
systemVersion,
|
|
180
|
+
appVersion: 'unknown',
|
|
181
|
+
buildNumber: 'unknown',
|
|
182
|
+
brand: Platform.OS === 'ios' ? 'Apple' : 'Unknown',
|
|
183
|
+
isEmulator: false,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
if (!moduleRef) {
|
|
187
|
+
return metadata;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
metadata.device = fallbackString(moduleRef.getModel?.(), metadata.device);
|
|
192
|
+
} catch {
|
|
193
|
+
// noop
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
metadata.systemName = fallbackString(moduleRef.getSystemName?.(), metadata.systemName);
|
|
197
|
+
} catch {
|
|
198
|
+
// noop
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
metadata.systemVersion = fallbackString(moduleRef.getSystemVersion?.(), metadata.systemVersion);
|
|
202
|
+
} catch {
|
|
203
|
+
// noop
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
metadata.appVersion = fallbackString(moduleRef.getVersion?.(), metadata.appVersion);
|
|
207
|
+
} catch {
|
|
208
|
+
// noop
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
metadata.buildNumber = fallbackString(moduleRef.getBuildNumber?.(), metadata.buildNumber);
|
|
212
|
+
} catch {
|
|
213
|
+
// noop
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
metadata.brand = fallbackString(moduleRef.getBrand?.(), metadata.brand);
|
|
217
|
+
} catch {
|
|
218
|
+
// noop
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
metadata.isEmulator = moduleRef.isEmulator ? await moduleRef.isEmulator() : false;
|
|
222
|
+
} catch {
|
|
223
|
+
metadata.isEmulator = false;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return metadata;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function hasScreenRecordingSupport(): boolean {
|
|
230
|
+
const moduleRef = getRecordScreenModule();
|
|
231
|
+
return Boolean(moduleRef?.startRecording && moduleRef?.stopRecording);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export async function startScreenRecording(config: { mic?: boolean } = { mic: true }): Promise<void> {
|
|
235
|
+
const moduleRef = getRecordScreenModule();
|
|
236
|
+
if (!moduleRef?.startRecording) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
'Screen recording dependency missing. Install react-native-record-screen and rebuild the app.'
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
await moduleRef.startRecording(config);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export async function stopScreenRecording(): Promise<unknown> {
|
|
245
|
+
const moduleRef = getRecordScreenModule();
|
|
246
|
+
if (!moduleRef?.stopRecording) {
|
|
247
|
+
throw new Error(
|
|
248
|
+
'Screen recording dependency missing. Install react-native-record-screen and rebuild the app.'
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
return moduleRef.stopRecording();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export async function captureScreenImage(options?: CaptureScreenOptions): Promise<string> {
|
|
255
|
+
const captureScreen = getCaptureScreenFn();
|
|
256
|
+
if (!captureScreen) {
|
|
257
|
+
throw new Error(
|
|
258
|
+
'Screenshot dependency missing. Install react-native-view-shot and rebuild the app.'
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
return captureScreen(options);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function hasAudioRecordingSupport(): boolean {
|
|
265
|
+
const moduleRef = getAudioRecorderModule();
|
|
266
|
+
const recorderCtor = moduleRef?.default;
|
|
267
|
+
return typeof recorderCtor === 'function';
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function createAudioRecorderPlayer(): AudioRecorderPlayerLike | null {
|
|
271
|
+
const moduleRef = getAudioRecorderModule();
|
|
272
|
+
const recorderCtor = moduleRef?.default;
|
|
273
|
+
if (typeof recorderCtor !== 'function') {
|
|
274
|
+
warnOnce(
|
|
275
|
+
'missing:react-native-audio-recorder-player:ctor',
|
|
276
|
+
'[iInstall SDK] Audio recording unavailable: react-native-audio-recorder-player is not linked correctly.'
|
|
277
|
+
);
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
return new recorderCtor();
|
|
283
|
+
} catch (error) {
|
|
284
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
285
|
+
warnOnce(
|
|
286
|
+
'missing:react-native-audio-recorder-player:new',
|
|
287
|
+
`[iInstall SDK] Failed to initialize audio recorder. (${reason})`
|
|
288
|
+
);
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function getAudioRecordingPreset(): Record<string, string | number> {
|
|
294
|
+
const moduleRef = getAudioRecorderModule();
|
|
295
|
+
return {
|
|
296
|
+
AVFormatIDKeyIOS: moduleRef?.AVEncodingOption?.aac ?? 'aac',
|
|
297
|
+
AVEncoderAudioQualityKeyIOS: moduleRef?.AVEncoderAudioQualityIOSType?.high ?? 96,
|
|
298
|
+
AVSampleRateKeyIOS: 44100,
|
|
299
|
+
AVNumberOfChannelsKeyIOS: 1,
|
|
300
|
+
AVEncoderBitRateKeyIOS: 128000,
|
|
301
|
+
AudioEncoderAndroid: moduleRef?.AudioEncoderAndroidType?.AAC ?? 3,
|
|
302
|
+
AudioSourceAndroid: moduleRef?.AudioSourceAndroidType?.MIC ?? 1,
|
|
303
|
+
OutputFormatAndroid: moduleRef?.OutputFormatAndroidType?.MPEG_4 ?? 2,
|
|
304
|
+
AudioEncodingBitRateAndroid: 128000,
|
|
305
|
+
AudioSamplingRateAndroid: 44100,
|
|
306
|
+
AudioChannelsAndroid: 1,
|
|
307
|
+
};
|
|
308
|
+
}
|