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.
@@ -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.13",
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
  }
@@ -16,15 +16,13 @@ import {
16
16
  TouchableWithoutFeedback,
17
17
  ScrollView
18
18
  } from 'react-native';
19
- import DeviceInfo from 'react-native-device-info';
20
- import AudioRecorderPlayer, {
21
- AVEncodingOption,
22
- AVEncoderAudioQualityIOSType,
23
- AudioEncoderAndroidType,
24
- AudioSourceAndroidType,
25
- OutputFormatAndroidType,
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(new AudioRecorderPlayer()).current;
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
- audioSet,
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
- formData.append('udid', await DeviceInfo.getUniqueId());
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={[styles.actionBtn, isRecordingAudio ? styles.recordingBtn : undefined, audioUri ? styles.hasAudioBtn : undefined]}
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
- {isRecordingAudio ? `Stop (${audioDuration})` : audioUri ? 'Re-record Audio' : '🎙 Record Audio'}
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
- DeviceInfo.isEmulator()
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 captureScreen({
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 DeviceInfo.getUniqueId().catch(() => undefined);
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 RecordScreen.startRecording({ mic: true });
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 RecordScreen.stopRecording();
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
+ }