react-native-iinstall 0.2.12 → 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.12",
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',
@@ -10,6 +10,10 @@ interface IInstallWrapperProps {
10
10
  showDebugButton?: boolean;
11
11
  showFloatingButtonOnEmulator?: boolean;
12
12
  floatingButtonLabel?: string;
13
+ pushToken?: string;
14
+ autoRegisterPushToken?: boolean;
15
+ projectId?: string;
16
+ onPushTokenRegisterError?: (error: string) => void;
13
17
  }
14
18
 
15
19
  // Backward-compatible wrapper. The core IInstall component now handles:
@@ -23,6 +27,10 @@ export const IInstallWrapper: React.FC<IInstallWrapperProps> = ({
23
27
  showDebugButton: _showDebugButton = false,
24
28
  showFloatingButtonOnEmulator = true,
25
29
  floatingButtonLabel = 'Report Issue',
30
+ pushToken,
31
+ autoRegisterPushToken = true,
32
+ projectId,
33
+ onPushTokenRegisterError,
26
34
  }) => {
27
35
  return (
28
36
  <IInstall
@@ -31,6 +39,10 @@ export const IInstallWrapper: React.FC<IInstallWrapperProps> = ({
31
39
  enabled={enabled}
32
40
  showFloatingButtonOnEmulator={showFloatingButtonOnEmulator}
33
41
  floatingButtonLabel={floatingButtonLabel}
42
+ pushToken={pushToken}
43
+ autoRegisterPushToken={autoRegisterPushToken}
44
+ projectId={projectId}
45
+ onPushTokenRegisterError={onPushTokenRegisterError}
34
46
  >
35
47
  {children}
36
48
  </IInstall>
package/src/index.tsx CHANGED
@@ -1,10 +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';
5
+ import { registerPushToken } from './pushRegistration';
6
+ import {
7
+ captureScreenImage,
8
+ getDeviceUniqueId,
9
+ hasScreenRecordingSupport,
10
+ isEmulatorDevice,
11
+ startScreenRecording,
12
+ stopScreenRecording,
13
+ } from './nativeModules';
8
14
 
9
15
  interface IInstallProps {
10
16
  apiKey: string;
@@ -13,6 +19,10 @@ interface IInstallProps {
13
19
  enabled?: boolean;
14
20
  showFloatingButtonOnEmulator?: boolean;
15
21
  floatingButtonLabel?: string;
22
+ pushToken?: string;
23
+ autoRegisterPushToken?: boolean;
24
+ projectId?: string;
25
+ onPushTokenRegisterError?: (error: string) => void;
16
26
  }
17
27
 
18
28
  export const IInstall: React.FC<IInstallProps> = ({
@@ -22,6 +32,10 @@ export const IInstall: React.FC<IInstallProps> = ({
22
32
  enabled = true,
23
33
  showFloatingButtonOnEmulator = true,
24
34
  floatingButtonLabel = 'Report Issue',
35
+ pushToken,
36
+ autoRegisterPushToken = true,
37
+ projectId,
38
+ onPushTokenRegisterError,
25
39
  }) => {
26
40
  const [modalVisible, setModalVisible] = useState(false);
27
41
  const [screenshotUri, setScreenshotUri] = useState<string | null>(null);
@@ -30,6 +44,7 @@ export const IInstall: React.FC<IInstallProps> = ({
30
44
  const [isEmulator, setIsEmulator] = useState(false);
31
45
 
32
46
  const shakeDetectorRef = useRef<ShakeDetector | null>(null);
47
+ const lastRegisteredPushTokenRef = useRef<string | null>(null);
33
48
 
34
49
  // Refs for stable access in shake callback
35
50
  const isRecordingRef = useRef(isRecording);
@@ -43,7 +58,7 @@ export const IInstall: React.FC<IInstallProps> = ({
43
58
  useEffect(() => {
44
59
  let mounted = true;
45
60
 
46
- DeviceInfo.isEmulator()
61
+ isEmulatorDevice()
47
62
  .then((value) => {
48
63
  if (mounted) {
49
64
  setIsEmulator(value);
@@ -65,7 +80,7 @@ export const IInstall: React.FC<IInstallProps> = ({
65
80
 
66
81
  try {
67
82
  // Capture screenshot
68
- const uri = await captureScreen({
83
+ const uri = await captureScreenImage({
69
84
  format: 'png',
70
85
  quality: 0.8,
71
86
  });
@@ -96,6 +111,58 @@ export const IInstall: React.FC<IInstallProps> = ({
96
111
  };
97
112
  }, [enabled, isEmulator]);
98
113
 
114
+ useEffect(() => {
115
+ if (!enabled || !autoRegisterPushToken || !pushToken) return;
116
+ if (lastRegisteredPushTokenRef.current === pushToken) return;
117
+
118
+ let cancelled = false;
119
+
120
+ const syncPushToken = async () => {
121
+ const deviceUdid = await getDeviceUniqueId();
122
+ const result = await registerPushToken({
123
+ token: pushToken,
124
+ apiKey,
125
+ apiEndpoint,
126
+ deviceUdid,
127
+ projectId,
128
+ });
129
+
130
+ if (cancelled) return;
131
+
132
+ if (result.success) {
133
+ lastRegisteredPushTokenRef.current = pushToken;
134
+ } else {
135
+ const message = result.error || 'Failed to register push token';
136
+ console.warn('IInstall: push token registration failed', message);
137
+ if (onPushTokenRegisterError) {
138
+ onPushTokenRegisterError(message);
139
+ }
140
+ }
141
+ };
142
+
143
+ syncPushToken().catch((error) => {
144
+ if (cancelled) return;
145
+ const message =
146
+ error instanceof Error ? error.message : 'Failed to register push token';
147
+ console.warn('IInstall: push token registration failed', message);
148
+ if (onPushTokenRegisterError) {
149
+ onPushTokenRegisterError(message);
150
+ }
151
+ });
152
+
153
+ return () => {
154
+ cancelled = true;
155
+ };
156
+ }, [
157
+ enabled,
158
+ autoRegisterPushToken,
159
+ pushToken,
160
+ apiKey,
161
+ apiEndpoint,
162
+ projectId,
163
+ onPushTokenRegisterError,
164
+ ]);
165
+
99
166
  const handleFloatingButtonPress = async () => {
100
167
  await handleShakeCallback.current();
101
168
  };
@@ -106,6 +173,7 @@ export const IInstall: React.FC<IInstallProps> = ({
106
173
  showFloatingButtonOnEmulator &&
107
174
  !modalVisible &&
108
175
  !isRecording;
176
+ const screenRecordingAvailable = hasScreenRecordingSupport();
109
177
 
110
178
  const normalizeVideoUri = (value: string) => {
111
179
  if (Platform.OS === 'ios' && value.startsWith('/')) {
@@ -180,10 +248,18 @@ export const IInstall: React.FC<IInstallProps> = ({
180
248
  };
181
249
 
182
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
+
183
259
  setModalVisible(false);
184
260
  setIsRecording(true);
185
261
  try {
186
- await RecordScreen.startRecording({ mic: true });
262
+ await startScreenRecording({ mic: true });
187
263
  } catch (e) {
188
264
  console.error('Failed to start recording', e);
189
265
  setIsRecording(false);
@@ -194,7 +270,7 @@ export const IInstall: React.FC<IInstallProps> = ({
194
270
  const handleStopRecording = async () => {
195
271
  try {
196
272
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
197
- const res: any = await RecordScreen.stopRecording();
273
+ const res: any = await stopScreenRecording();
198
274
  if (res) {
199
275
  const nextVideoUri = resolveRecordedVideoUri(res);
200
276
  if (nextVideoUri) {
@@ -253,7 +329,7 @@ export const IInstall: React.FC<IInstallProps> = ({
253
329
  }}
254
330
  screenshotUri={screenshotUri}
255
331
  videoUri={videoUri}
256
- onStartRecording={handleStartRecording}
332
+ onStartRecording={screenRecordingAvailable ? handleStartRecording : undefined}
257
333
  apiKey={apiKey}
258
334
  apiEndpoint={apiEndpoint}
259
335
  />
@@ -329,3 +405,11 @@ export default IInstall;
329
405
 
330
406
  // Export the wrapper component for simulator/emulator support
331
407
  export { IInstallWrapper } from './IInstallWrapper';
408
+ export {
409
+ registerPushToken,
410
+ unregisterPushToken,
411
+ type RegisterPushTokenParams,
412
+ type UnregisterPushTokenParams,
413
+ type PushRegistrationResult,
414
+ type PushPlatform,
415
+ } from './pushRegistration';