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.
- package/INTEGRATION_GUIDE.md +43 -5
- package/README.md +37 -2
- package/RELEASE_MANIFEST.json +8 -0
- package/lib/IInstallWrapper.d.ts +4 -0
- package/lib/IInstallWrapper.js +2 -2
- package/lib/index.d.ts +5 -0
- package/lib/index.js +56 -2
- package/lib/pushRegistration.d.ts +22 -0
- package/lib/pushRegistration.js +70 -0
- package/lib/scripts/check-release-manifest.d.mts +2 -0
- package/lib/scripts/check-release-manifest.mjs +38 -0
- package/lib/scripts/sync-release-manifest.d.mts +2 -0
- package/lib/scripts/sync-release-manifest.mjs +38 -0
- package/lib/src/FeedbackModal.d.ts +12 -0
- package/lib/src/FeedbackModal.js +393 -0
- package/lib/src/IInstallWrapper.d.ts +16 -0
- package/lib/src/IInstallWrapper.js +18 -0
- package/lib/src/ShakeDetector.d.ts +8 -0
- package/lib/src/ShakeDetector.js +68 -0
- package/lib/src/index.d.ts +17 -0
- package/lib/src/index.js +349 -0
- package/lib/src/nativeModules.d.ts +37 -0
- package/lib/src/nativeModules.js +231 -0
- package/lib/src/pushRegistration.d.ts +22 -0
- package/lib/src/pushRegistration.js +70 -0
- package/package.json +18 -8
- package/src/FeedbackModal.tsx +46 -41
- package/src/IInstallWrapper.tsx +12 -0
- package/src/index.tsx +92 -8
- package/src/nativeModules.ts +308 -0
- package/src/pushRegistration.ts +111 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerPushToken = registerPushToken;
|
|
4
|
+
exports.unregisterPushToken = unregisterPushToken;
|
|
5
|
+
const react_native_1 = require("react-native");
|
|
6
|
+
function normalizeApiEndpoint(apiEndpoint) {
|
|
7
|
+
return apiEndpoint.replace(/\/+$/, '');
|
|
8
|
+
}
|
|
9
|
+
function resolvePlatform(platform) {
|
|
10
|
+
if (platform)
|
|
11
|
+
return platform;
|
|
12
|
+
return react_native_1.Platform.OS === 'ios' ? 'IOS' : 'ANDROID';
|
|
13
|
+
}
|
|
14
|
+
async function registerPushToken({ token, apiKey, apiEndpoint = 'https://iinstall.app', deviceUdid, projectId, platform, }) {
|
|
15
|
+
try {
|
|
16
|
+
const response = await fetch(`${normalizeApiEndpoint(apiEndpoint)}/api/notifications/push/register`, {
|
|
17
|
+
method: 'POST',
|
|
18
|
+
headers: { 'Content-Type': 'application/json' },
|
|
19
|
+
body: JSON.stringify({
|
|
20
|
+
token,
|
|
21
|
+
apiKey,
|
|
22
|
+
deviceUdid,
|
|
23
|
+
projectId,
|
|
24
|
+
platform: resolvePlatform(platform),
|
|
25
|
+
}),
|
|
26
|
+
});
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
28
|
+
const data = await response.json().catch(() => ({}));
|
|
29
|
+
return {
|
|
30
|
+
success: response.ok,
|
|
31
|
+
status: response.status,
|
|
32
|
+
data,
|
|
33
|
+
error: response.ok ? undefined : data?.error || 'Failed to register push token',
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
return {
|
|
38
|
+
success: false,
|
|
39
|
+
status: 500,
|
|
40
|
+
error: error instanceof Error ? error.message : 'Unknown register push error',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function unregisterPushToken({ token, apiKey, apiEndpoint = 'https://iinstall.app', }) {
|
|
45
|
+
try {
|
|
46
|
+
const response = await fetch(`${normalizeApiEndpoint(apiEndpoint)}/api/notifications/push/register`, {
|
|
47
|
+
method: 'DELETE',
|
|
48
|
+
headers: { 'Content-Type': 'application/json' },
|
|
49
|
+
body: JSON.stringify({
|
|
50
|
+
token,
|
|
51
|
+
apiKey,
|
|
52
|
+
}),
|
|
53
|
+
});
|
|
54
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
55
|
+
const data = await response.json().catch(() => ({}));
|
|
56
|
+
return {
|
|
57
|
+
success: response.ok,
|
|
58
|
+
status: response.status,
|
|
59
|
+
data,
|
|
60
|
+
error: response.ok ? undefined : data?.error || 'Failed to unregister push token',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
return {
|
|
65
|
+
success: false,
|
|
66
|
+
status: 500,
|
|
67
|
+
error: error instanceof Error ? error.message : 'Unknown unregister push error',
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-iinstall",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.14",
|
|
4
4
|
"description": "🎯 IInstall React Native SDK - The ultimate beta testing & QA feedback tool. Shake-to-report with voice recordings, screen recordings, and screenshots. Zero-config setup with TypeScript support. Perfect for beta testing, QA teams, and user feedback collection.",
|
|
5
5
|
"author": "TesterFlow Team",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,22 +34,27 @@
|
|
|
34
34
|
"files": [
|
|
35
35
|
"lib",
|
|
36
36
|
"src",
|
|
37
|
-
"INTEGRATION_GUIDE.md"
|
|
37
|
+
"INTEGRATION_GUIDE.md",
|
|
38
|
+
"RELEASE_MANIFEST.json"
|
|
38
39
|
],
|
|
39
40
|
"scripts": {
|
|
41
|
+
"prebuild": "npm run prepare:release",
|
|
40
42
|
"build": "tsc",
|
|
43
|
+
"sync:release-manifest": "node scripts/sync-release-manifest.mjs",
|
|
44
|
+
"check:release-manifest": "node scripts/check-release-manifest.mjs",
|
|
45
|
+
"prepare:release": "npm run sync:release-manifest && npm run check:release-manifest",
|
|
41
46
|
"prepare": "npm run build"
|
|
42
47
|
},
|
|
43
48
|
"peerDependencies": {
|
|
44
49
|
"react": ">=16.8.0",
|
|
45
|
-
"react-native": ">=0.60.0"
|
|
46
|
-
},
|
|
47
|
-
"dependencies": {
|
|
48
|
-
"react-native-sensors": "^7.3.0",
|
|
49
|
-
"react-native-view-shot": "^3.1.2",
|
|
50
|
-
"react-native-device-info": "^10.0.0",
|
|
50
|
+
"react-native": ">=0.60.0",
|
|
51
51
|
"react-native-audio-recorder-player": "^3.6.4",
|
|
52
|
+
"react-native-device-info": "^10.0.0",
|
|
52
53
|
"react-native-record-screen": "^0.6.2",
|
|
54
|
+
"react-native-sensors": "^7.3.0",
|
|
55
|
+
"react-native-view-shot": "^3.1.2"
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
53
58
|
"rxjs": "^7.0.0"
|
|
54
59
|
},
|
|
55
60
|
"devDependencies": {
|
|
@@ -57,6 +62,11 @@
|
|
|
57
62
|
"@types/react-native": "^0.70.0",
|
|
58
63
|
"react": "18.2.0",
|
|
59
64
|
"react-native": "0.72.6",
|
|
65
|
+
"react-native-audio-recorder-player": "^3.6.4",
|
|
66
|
+
"react-native-device-info": "^10.0.0",
|
|
67
|
+
"react-native-record-screen": "^0.6.2",
|
|
68
|
+
"react-native-sensors": "^7.3.0",
|
|
69
|
+
"react-native-view-shot": "^3.1.2",
|
|
60
70
|
"typescript": "^5.0.0"
|
|
61
71
|
}
|
|
62
72
|
}
|
package/src/FeedbackModal.tsx
CHANGED
|
@@ -16,15 +16,13 @@ import {
|
|
|
16
16
|
TouchableWithoutFeedback,
|
|
17
17
|
ScrollView
|
|
18
18
|
} from 'react-native';
|
|
19
|
-
import
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
type AudioSet,
|
|
27
|
-
} from 'react-native-audio-recorder-player';
|
|
19
|
+
import {
|
|
20
|
+
createAudioRecorderPlayer,
|
|
21
|
+
getAudioRecordingPreset,
|
|
22
|
+
getDeviceMetadata,
|
|
23
|
+
getDeviceUniqueId,
|
|
24
|
+
hasAudioRecordingSupport,
|
|
25
|
+
} from './nativeModules';
|
|
28
26
|
|
|
29
27
|
interface FeedbackModalProps {
|
|
30
28
|
visible: boolean;
|
|
@@ -52,19 +50,28 @@ export const FeedbackModal: React.FC<FeedbackModalProps> = ({
|
|
|
52
50
|
const [isRecordingAudio, setIsRecordingAudio] = useState(false);
|
|
53
51
|
const [audioUri, setAudioUri] = useState<string | null>(null);
|
|
54
52
|
const [audioDuration, setAudioDuration] = useState('00:00');
|
|
53
|
+
const audioRecordingAvailable = hasAudioRecordingSupport();
|
|
55
54
|
|
|
56
|
-
const audioRecorderPlayer = useRef(
|
|
55
|
+
const audioRecorderPlayer = useRef(createAudioRecorderPlayer()).current;
|
|
57
56
|
|
|
58
57
|
useEffect(() => {
|
|
59
58
|
return () => {
|
|
60
59
|
// Cleanup
|
|
61
|
-
if (isRecordingAudio) {
|
|
62
|
-
audioRecorderPlayer.stopRecorder();
|
|
60
|
+
if (isRecordingAudio && audioRecorderPlayer) {
|
|
61
|
+
audioRecorderPlayer.stopRecorder().catch(() => undefined);
|
|
63
62
|
}
|
|
64
63
|
};
|
|
65
|
-
}, [isRecordingAudio]);
|
|
64
|
+
}, [audioRecorderPlayer, isRecordingAudio]);
|
|
66
65
|
|
|
67
66
|
const onStartAudioRecord = async () => {
|
|
67
|
+
if (!audioRecorderPlayer || !audioRecordingAvailable) {
|
|
68
|
+
Alert.alert(
|
|
69
|
+
'Audio unavailable',
|
|
70
|
+
'Install react-native-audio-recorder-player in your app root, then rebuild the app.'
|
|
71
|
+
);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
68
75
|
if (Platform.OS === 'android') {
|
|
69
76
|
try {
|
|
70
77
|
const grants = await PermissionsAndroid.requestMultiple([
|
|
@@ -86,23 +93,9 @@ export const FeedbackModal: React.FC<FeedbackModalProps> = ({
|
|
|
86
93
|
}
|
|
87
94
|
|
|
88
95
|
try {
|
|
89
|
-
const audioSet: AudioSet = {
|
|
90
|
-
AVFormatIDKeyIOS: AVEncodingOption.aac,
|
|
91
|
-
AVEncoderAudioQualityKeyIOS: AVEncoderAudioQualityIOSType.high,
|
|
92
|
-
AVSampleRateKeyIOS: 44100,
|
|
93
|
-
AVNumberOfChannelsKeyIOS: 1,
|
|
94
|
-
AVEncoderBitRateKeyIOS: 128000,
|
|
95
|
-
AudioEncoderAndroid: AudioEncoderAndroidType.AAC,
|
|
96
|
-
AudioSourceAndroid: AudioSourceAndroidType.MIC,
|
|
97
|
-
OutputFormatAndroid: OutputFormatAndroidType.MPEG_4,
|
|
98
|
-
AudioEncodingBitRateAndroid: 128000,
|
|
99
|
-
AudioSamplingRateAndroid: 44100,
|
|
100
|
-
AudioChannelsAndroid: 1,
|
|
101
|
-
};
|
|
102
|
-
|
|
103
96
|
const result = await audioRecorderPlayer.startRecorder(
|
|
104
97
|
undefined,
|
|
105
|
-
|
|
98
|
+
getAudioRecordingPreset(),
|
|
106
99
|
);
|
|
107
100
|
audioRecorderPlayer.addRecordBackListener((e) => {
|
|
108
101
|
setAudioDuration(audioRecorderPlayer.mmssss(Math.floor(e.currentPosition)));
|
|
@@ -117,6 +110,10 @@ export const FeedbackModal: React.FC<FeedbackModalProps> = ({
|
|
|
117
110
|
};
|
|
118
111
|
|
|
119
112
|
const onStopAudioRecord = async () => {
|
|
113
|
+
if (!audioRecorderPlayer) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
120
117
|
try {
|
|
121
118
|
const result = await audioRecorderPlayer.stopRecorder();
|
|
122
119
|
audioRecorderPlayer.removeRecordBackListener();
|
|
@@ -134,7 +131,7 @@ export const FeedbackModal: React.FC<FeedbackModalProps> = ({
|
|
|
134
131
|
};
|
|
135
132
|
|
|
136
133
|
const handleClose = async () => {
|
|
137
|
-
if (isRecordingAudio) {
|
|
134
|
+
if (isRecordingAudio && audioRecorderPlayer) {
|
|
138
135
|
try {
|
|
139
136
|
await audioRecorderPlayer.stopRecorder();
|
|
140
137
|
} catch (error) {
|
|
@@ -203,21 +200,14 @@ export const FeedbackModal: React.FC<FeedbackModalProps> = ({
|
|
|
203
200
|
}
|
|
204
201
|
|
|
205
202
|
// 4. Metadata
|
|
206
|
-
const metadata =
|
|
207
|
-
device: DeviceInfo.getModel(),
|
|
208
|
-
systemName: DeviceInfo.getSystemName(),
|
|
209
|
-
systemVersion: DeviceInfo.getSystemVersion(),
|
|
210
|
-
appVersion: DeviceInfo.getVersion(),
|
|
211
|
-
buildNumber: DeviceInfo.getBuildNumber(),
|
|
212
|
-
brand: DeviceInfo.getBrand(),
|
|
213
|
-
isEmulator: await DeviceInfo.isEmulator(),
|
|
214
|
-
};
|
|
203
|
+
const metadata = await getDeviceMetadata();
|
|
215
204
|
formData.append('metadata', JSON.stringify(metadata));
|
|
216
205
|
|
|
217
206
|
// 5. Other fields
|
|
218
207
|
formData.append('description', description);
|
|
219
208
|
formData.append('apiKey', apiKey);
|
|
220
|
-
|
|
209
|
+
const deviceUdid = await getDeviceUniqueId();
|
|
210
|
+
formData.append('udid', deviceUdid || 'unknown');
|
|
221
211
|
|
|
222
212
|
// 6. Send
|
|
223
213
|
const response = await fetch(`${apiEndpoint}/api/feedback`, {
|
|
@@ -306,11 +296,23 @@ export const FeedbackModal: React.FC<FeedbackModalProps> = ({
|
|
|
306
296
|
<View style={styles.actionsRow}>
|
|
307
297
|
{/* Audio Recorder */}
|
|
308
298
|
<TouchableOpacity
|
|
309
|
-
style={[
|
|
299
|
+
style={[
|
|
300
|
+
styles.actionBtn,
|
|
301
|
+
isRecordingAudio ? styles.recordingBtn : undefined,
|
|
302
|
+
audioUri ? styles.hasAudioBtn : undefined,
|
|
303
|
+
!audioRecordingAvailable ? styles.disabledActionBtn : undefined,
|
|
304
|
+
]}
|
|
310
305
|
onPress={isRecordingAudio ? onStopAudioRecord : onStartAudioRecord}
|
|
306
|
+
disabled={!audioRecordingAvailable}
|
|
311
307
|
>
|
|
312
308
|
<Text style={[styles.actionBtnText, (isRecordingAudio || audioUri) ? { color: '#FFF' } : undefined]}>
|
|
313
|
-
{
|
|
309
|
+
{!audioRecordingAvailable
|
|
310
|
+
? '🎙 Audio Unavailable'
|
|
311
|
+
: isRecordingAudio
|
|
312
|
+
? `Stop (${audioDuration})`
|
|
313
|
+
: audioUri
|
|
314
|
+
? 'Re-record Audio'
|
|
315
|
+
: '🎙 Record Audio'}
|
|
314
316
|
</Text>
|
|
315
317
|
</TouchableOpacity>
|
|
316
318
|
|
|
@@ -437,6 +439,9 @@ const styles = StyleSheet.create({
|
|
|
437
439
|
hasAudioBtn: {
|
|
438
440
|
backgroundColor: '#4CAF50',
|
|
439
441
|
},
|
|
442
|
+
disabledActionBtn: {
|
|
443
|
+
opacity: 0.45,
|
|
444
|
+
},
|
|
440
445
|
actionBtnText: {
|
|
441
446
|
fontSize: 14,
|
|
442
447
|
fontWeight: '600',
|
package/src/IInstallWrapper.tsx
CHANGED
|
@@ -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
|
|
7
|
-
import
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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';
|