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.
@@ -46,16 +46,20 @@ npm install react-native-iinstall
46
46
  yarn add react-native-iinstall
47
47
  ```
48
48
 
49
- **Note**: The SDK automatically includes necessary dependencies like `react-native-sensors` and `react-native-view-shot`.
49
+ Install required native peer dependencies at app root:
50
50
 
51
- ### Updating to Latest Version (v0.2.13)
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.13
60
+ npm install react-native-iinstall@0.2.14
57
61
  # OR
58
- yarn add react-native-iinstall@0.2.13
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.7+)
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.7+ which fixes audio codec compatibility. Audio files are now encoded as AAC instead of ALAC for better browser compatibility.
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.7+ which includes enhanced URI extraction and better error handling for screen recordings across different devices.
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
- **Note**: This package automatically installs required dependencies (`react-native-sensors`, `react-native-view-shot`, etc.).
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
- 2. Link native modules (iOS only, non-Expo):
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.7+ for AAC codec support
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
+ }
@@ -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 react_native_device_info_1 = __importDefault(require("react-native-device-info"));
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 audioRecorderPlayer = (0, react_1.useRef)(new react_native_audio_recorder_player_1.default()).current;
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 audioSet = {
81
- AVFormatIDKeyIOS: react_native_audio_recorder_player_1.AVEncodingOption.aac,
82
- AVEncoderAudioQualityKeyIOS: react_native_audio_recorder_player_1.AVEncoderAudioQualityIOSType.high,
83
- AVSampleRateKeyIOS: 44100,
84
- AVNumberOfChannelsKeyIOS: 1,
85
- AVEncoderBitRateKeyIOS: 128000,
86
- AudioEncoderAndroid: react_native_audio_recorder_player_1.AudioEncoderAndroidType.AAC,
87
- AudioSourceAndroid: react_native_audio_recorder_player_1.AudioSourceAndroidType.MIC,
88
- OutputFormatAndroid: react_native_audio_recorder_player_1.OutputFormatAndroidType.MPEG_4,
89
- AudioEncodingBitRateAndroid: 128000,
90
- AudioSamplingRateAndroid: 44100,
91
- AudioChannelsAndroid: 1,
92
- };
93
- const result = await audioRecorderPlayer.startRecorder(undefined, audioSet);
81
+ 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
- formData.append('udid', await react_native_device_info_1.default.getUniqueId());
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={[styles.actionBtn, isRecordingAudio ? styles.recordingBtn : undefined, audioUri ? styles.hasAudioBtn : undefined]} onPress={isRecordingAudio ? onStopAudioRecord : onStartAudioRecord}>
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
- {isRecordingAudio ? `Stop (${audioDuration})` : audioUri ? 'Re-record Audio' : '🎙 Record Audio'}
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
- react_native_device_info_1.default.isEmulator()
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, react_native_view_shot_1.captureScreen)({
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 react_native_device_info_1.default.getUniqueId().catch(() => undefined);
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 react_native_record_screen_1.default.startRecording({ mic: true });
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 react_native_record_screen_1.default.stopRecording();
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.13",
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
- "build": "tsc",
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
  }
@@ -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,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
+ }