react-native-iinstall 0.2.13 → 0.2.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -2
- package/RELEASE_MANIFEST.json +8 -0
- package/lib/scripts/check-release-manifest.d.mts +2 -0
- package/lib/scripts/check-release-manifest.mjs +38 -0
- package/lib/scripts/sync-release-manifest.d.mts +2 -0
- package/lib/scripts/sync-release-manifest.mjs +38 -0
- package/lib/src/FeedbackModal.d.ts +12 -0
- package/lib/src/FeedbackModal.js +393 -0
- package/lib/src/IInstallWrapper.d.ts +16 -0
- package/lib/src/IInstallWrapper.js +18 -0
- package/lib/src/ShakeDetector.d.ts +8 -0
- package/lib/src/ShakeDetector.js +68 -0
- package/lib/src/index.d.ts +17 -0
- package/lib/src/index.js +349 -0
- package/lib/src/nativeModules.d.ts +37 -0
- package/lib/src/nativeModules.js +231 -0
- package/lib/src/pushRegistration.d.ts +22 -0
- package/lib/src/pushRegistration.js +70 -0
- package/package.json +18 -8
- package/src/FeedbackModal.tsx +46 -41
- package/src/index.tsx +23 -9
- package/src/nativeModules.ts +308 -0
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
|
-
|
|
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
|
-
|
|
42
|
+
3. Link native modules (iOS only, non-Expo):
|
|
40
43
|
```bash
|
|
41
44
|
cd ios && pod install
|
|
42
45
|
```
|
|
@@ -174,6 +177,7 @@ await unregisterPushToken({
|
|
|
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
179
|
- **Audio issues?** Ensure SDK v0.2.7+ for AAC codec support
|
|
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,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
const cwd = process.cwd();
|
|
6
|
+
const packagePath = path.join(cwd, 'package.json');
|
|
7
|
+
const manifestPath = path.join(cwd, 'RELEASE_MANIFEST.json');
|
|
8
|
+
function readJson(filePath) {
|
|
9
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
10
|
+
return JSON.parse(raw);
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
if (!fs.existsSync(manifestPath)) {
|
|
14
|
+
console.error('RELEASE_MANIFEST.json not found');
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
const pkg = readJson(packagePath);
|
|
18
|
+
const manifest = readJson(manifestPath);
|
|
19
|
+
const packageVersion = String(pkg?.version || '').trim();
|
|
20
|
+
const manifestVersion = String(manifest?.version || '').trim();
|
|
21
|
+
if (!packageVersion) {
|
|
22
|
+
console.error('SDK package version missing in package.json');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
if (!manifestVersion) {
|
|
26
|
+
console.error('RELEASE_MANIFEST version missing');
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
if (packageVersion !== manifestVersion) {
|
|
30
|
+
console.error(`Release mismatch: package.json=${packageVersion}, RELEASE_MANIFEST.json=${manifestVersion}`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
console.log(`Release manifest check OK: react-native-iinstall@${packageVersion}`);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
console.error('Failed to verify RELEASE_MANIFEST:', error);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
const cwd = process.cwd();
|
|
6
|
+
const packagePath = path.join(cwd, 'package.json');
|
|
7
|
+
const manifestPath = path.join(cwd, 'RELEASE_MANIFEST.json');
|
|
8
|
+
function readJson(filePath) {
|
|
9
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
10
|
+
return JSON.parse(raw);
|
|
11
|
+
}
|
|
12
|
+
function writeJson(filePath, value) {
|
|
13
|
+
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const pkg = readJson(packagePath);
|
|
17
|
+
const packageVersion = String(pkg?.version || '').trim();
|
|
18
|
+
if (!packageVersion) {
|
|
19
|
+
console.error('SDK package version missing in package.json');
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
let manifest = {
|
|
23
|
+
version: packageVersion,
|
|
24
|
+
highlights: [],
|
|
25
|
+
};
|
|
26
|
+
if (fs.existsSync(manifestPath)) {
|
|
27
|
+
manifest = {
|
|
28
|
+
...readJson(manifestPath),
|
|
29
|
+
version: packageVersion,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
writeJson(manifestPath, manifest);
|
|
33
|
+
console.log(`Synced RELEASE_MANIFEST -> react-native-iinstall@${packageVersion}`);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
console.error('Failed to sync RELEASE_MANIFEST:', error);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface FeedbackModalProps {
|
|
3
|
+
visible: boolean;
|
|
4
|
+
onClose: () => void;
|
|
5
|
+
screenshotUri: string | null;
|
|
6
|
+
videoUri?: string | null;
|
|
7
|
+
onStartRecording?: () => void;
|
|
8
|
+
apiKey: string;
|
|
9
|
+
apiEndpoint: string;
|
|
10
|
+
}
|
|
11
|
+
export declare const FeedbackModal: React.FC<FeedbackModalProps>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.FeedbackModal = void 0;
|
|
37
|
+
const react_1 = __importStar(require("react"));
|
|
38
|
+
const react_native_1 = require("react-native");
|
|
39
|
+
const nativeModules_1 = require("./nativeModules");
|
|
40
|
+
const FeedbackModal = ({ visible, onClose, screenshotUri, videoUri, onStartRecording, apiKey, apiEndpoint }) => {
|
|
41
|
+
const [description, setDescription] = (0, react_1.useState)('');
|
|
42
|
+
const [isSending, setIsSending] = (0, react_1.useState)(false);
|
|
43
|
+
// Audio State
|
|
44
|
+
const [isRecordingAudio, setIsRecordingAudio] = (0, react_1.useState)(false);
|
|
45
|
+
const [audioUri, setAudioUri] = (0, react_1.useState)(null);
|
|
46
|
+
const [audioDuration, setAudioDuration] = (0, react_1.useState)('00:00');
|
|
47
|
+
const audioRecordingAvailable = (0, nativeModules_1.hasAudioRecordingSupport)();
|
|
48
|
+
const audioRecorderPlayer = (0, react_1.useRef)((0, nativeModules_1.createAudioRecorderPlayer)()).current;
|
|
49
|
+
(0, react_1.useEffect)(() => {
|
|
50
|
+
return () => {
|
|
51
|
+
// Cleanup
|
|
52
|
+
if (isRecordingAudio && audioRecorderPlayer) {
|
|
53
|
+
audioRecorderPlayer.stopRecorder().catch(() => undefined);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}, [audioRecorderPlayer, isRecordingAudio]);
|
|
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
|
+
}
|
|
62
|
+
if (react_native_1.Platform.OS === 'android') {
|
|
63
|
+
try {
|
|
64
|
+
const grants = await react_native_1.PermissionsAndroid.requestMultiple([
|
|
65
|
+
react_native_1.PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
|
|
66
|
+
react_native_1.PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE,
|
|
67
|
+
react_native_1.PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
|
|
68
|
+
]);
|
|
69
|
+
// Note: Android 13+ might need different permissions for media, but keeping it simple
|
|
70
|
+
if (grants['android.permission.RECORD_AUDIO'] !== react_native_1.PermissionsAndroid.RESULTS.GRANTED) {
|
|
71
|
+
react_native_1.Alert.alert('Permission needed', 'Audio recording permission is required.');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
console.warn(err);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
const result = await audioRecorderPlayer.startRecorder(undefined, (0, nativeModules_1.getAudioRecordingPreset)());
|
|
82
|
+
audioRecorderPlayer.addRecordBackListener((e) => {
|
|
83
|
+
setAudioDuration(audioRecorderPlayer.mmssss(Math.floor(e.currentPosition)));
|
|
84
|
+
return;
|
|
85
|
+
});
|
|
86
|
+
setIsRecordingAudio(true);
|
|
87
|
+
setAudioUri(null);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
console.error('Failed to start recording', error);
|
|
91
|
+
react_native_1.Alert.alert('Error', 'Could not start audio recording');
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
const onStopAudioRecord = async () => {
|
|
95
|
+
if (!audioRecorderPlayer) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
const result = await audioRecorderPlayer.stopRecorder();
|
|
100
|
+
audioRecorderPlayer.removeRecordBackListener();
|
|
101
|
+
setIsRecordingAudio(false);
|
|
102
|
+
setAudioUri(result);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
console.error('Failed to stop recording', error);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
const resetDraft = () => {
|
|
109
|
+
setDescription('');
|
|
110
|
+
setAudioUri(null);
|
|
111
|
+
setAudioDuration('00:00');
|
|
112
|
+
};
|
|
113
|
+
const handleClose = async () => {
|
|
114
|
+
if (isRecordingAudio && audioRecorderPlayer) {
|
|
115
|
+
try {
|
|
116
|
+
await audioRecorderPlayer.stopRecorder();
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
console.warn('Failed to stop recorder during close', error);
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
audioRecorderPlayer.removeRecordBackListener();
|
|
123
|
+
setIsRecordingAudio(false);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
resetDraft();
|
|
127
|
+
onClose();
|
|
128
|
+
};
|
|
129
|
+
const sendFeedback = async () => {
|
|
130
|
+
if (!description.trim() && !audioUri && !videoUri) {
|
|
131
|
+
react_native_1.Alert.alert('Please provide a description, audio, or video.');
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
setIsSending(true);
|
|
135
|
+
try {
|
|
136
|
+
const formData = new FormData();
|
|
137
|
+
const normalizeUploadUri = (uri) => {
|
|
138
|
+
if (react_native_1.Platform.OS === 'ios' && uri.startsWith('/')) {
|
|
139
|
+
return `file://${uri}`;
|
|
140
|
+
}
|
|
141
|
+
return uri;
|
|
142
|
+
};
|
|
143
|
+
// 1. Screenshot
|
|
144
|
+
if (screenshotUri) {
|
|
145
|
+
const filename = screenshotUri.split('/').pop();
|
|
146
|
+
const match = /\.(\w+)$/.exec(filename || '');
|
|
147
|
+
const type = match ? `image/${match[1]}` : `image/png`;
|
|
148
|
+
formData.append('screenshot', {
|
|
149
|
+
uri: screenshotUri,
|
|
150
|
+
name: filename || 'screenshot.png',
|
|
151
|
+
type,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
// 2. Audio
|
|
155
|
+
if (audioUri) {
|
|
156
|
+
const normalizedAudioUri = normalizeUploadUri(audioUri);
|
|
157
|
+
formData.append('audio', {
|
|
158
|
+
uri: normalizedAudioUri,
|
|
159
|
+
name: 'feedback.m4a',
|
|
160
|
+
type: 'audio/mp4',
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
// 3. Video
|
|
164
|
+
if (videoUri) {
|
|
165
|
+
const normalizedVideoUri = normalizeUploadUri(videoUri);
|
|
166
|
+
const videoFilename = normalizedVideoUri.split('/').pop() || 'screen_record.mp4';
|
|
167
|
+
const videoType = /\.mov$/i.test(videoFilename) ? 'video/quicktime' : 'video/mp4';
|
|
168
|
+
formData.append('video', {
|
|
169
|
+
uri: normalizedVideoUri,
|
|
170
|
+
name: videoFilename,
|
|
171
|
+
type: videoType,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
// 4. Metadata
|
|
175
|
+
const metadata = await (0, nativeModules_1.getDeviceMetadata)();
|
|
176
|
+
formData.append('metadata', JSON.stringify(metadata));
|
|
177
|
+
// 5. Other fields
|
|
178
|
+
formData.append('description', description);
|
|
179
|
+
formData.append('apiKey', apiKey);
|
|
180
|
+
const deviceUdid = await (0, nativeModules_1.getDeviceUniqueId)();
|
|
181
|
+
formData.append('udid', deviceUdid || 'unknown');
|
|
182
|
+
// 6. Send
|
|
183
|
+
const response = await fetch(`${apiEndpoint}/api/feedback`, {
|
|
184
|
+
method: 'POST',
|
|
185
|
+
body: formData,
|
|
186
|
+
});
|
|
187
|
+
const result = await response.json();
|
|
188
|
+
if (response.ok) {
|
|
189
|
+
react_native_1.Alert.alert('Success', 'Feedback sent successfully!');
|
|
190
|
+
resetDraft();
|
|
191
|
+
onClose();
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
throw new Error(result.error || 'Failed to send');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
console.error('TesterFlow Error:', error);
|
|
199
|
+
react_native_1.Alert.alert('Error', 'Failed to send feedback. Please try again.');
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
setIsSending(false);
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
if (!visible)
|
|
206
|
+
return null;
|
|
207
|
+
const dismissKeyboard = () => {
|
|
208
|
+
react_native_1.Keyboard.dismiss();
|
|
209
|
+
};
|
|
210
|
+
return (<react_native_1.Modal visible={visible} transparent animationType="slide" onRequestClose={handleClose}>
|
|
211
|
+
<react_native_1.TouchableWithoutFeedback onPress={dismissKeyboard}>
|
|
212
|
+
<react_native_1.KeyboardAvoidingView behavior={react_native_1.Platform.OS === 'ios' ? 'padding' : 'height'} style={styles.container}>
|
|
213
|
+
<react_native_1.ScrollView contentContainerStyle={styles.scrollContainer} keyboardShouldPersistTaps="handled" showsVerticalScrollIndicator={false}>
|
|
214
|
+
<react_native_1.View style={styles.card}>
|
|
215
|
+
<react_native_1.View style={styles.header}>
|
|
216
|
+
<react_native_1.Text style={styles.title}>Report an Issue</react_native_1.Text>
|
|
217
|
+
<react_native_1.TouchableOpacity onPress={handleClose}>
|
|
218
|
+
<react_native_1.Text style={styles.closeBtn}>✕</react_native_1.Text>
|
|
219
|
+
</react_native_1.TouchableOpacity>
|
|
220
|
+
</react_native_1.View>
|
|
221
|
+
|
|
222
|
+
{/* Media Previews */}
|
|
223
|
+
<react_native_1.View style={styles.mediaRow}>
|
|
224
|
+
{/* Screenshot Preview */}
|
|
225
|
+
{screenshotUri && !videoUri && (<react_native_1.View style={styles.mediaItem}>
|
|
226
|
+
<react_native_1.Image source={{ uri: screenshotUri }} style={styles.mediaPreview} resizeMode="cover"/>
|
|
227
|
+
<react_native_1.View style={styles.mediaBadge}><react_native_1.Text style={styles.mediaBadgeText}>Screenshot</react_native_1.Text></react_native_1.View>
|
|
228
|
+
</react_native_1.View>)}
|
|
229
|
+
|
|
230
|
+
{/* Video Preview (Placeholder) */}
|
|
231
|
+
{videoUri && (<react_native_1.View style={styles.mediaItem}>
|
|
232
|
+
<react_native_1.View style={[styles.mediaPreview, { backgroundColor: '#000', justifyContent: 'center', alignItems: 'center' }]}>
|
|
233
|
+
<react_native_1.Text style={{ color: '#FFF' }}>▶ Video</react_native_1.Text>
|
|
234
|
+
</react_native_1.View>
|
|
235
|
+
<react_native_1.View style={styles.mediaBadge}><react_native_1.Text style={styles.mediaBadgeText}>Screen Rec</react_native_1.Text></react_native_1.View>
|
|
236
|
+
</react_native_1.View>)}
|
|
237
|
+
</react_native_1.View>
|
|
238
|
+
|
|
239
|
+
<react_native_1.TextInput style={styles.input} placeholder="What happened? Describe the bug..." placeholderTextColor="#999" multiline value={description} onChangeText={setDescription} returnKeyType="done" blurOnSubmit={true} onSubmitEditing={() => react_native_1.Keyboard.dismiss()}/>
|
|
240
|
+
|
|
241
|
+
{/* Media Actions Row */}
|
|
242
|
+
<react_native_1.View style={styles.actionsRow}>
|
|
243
|
+
{/* Audio Recorder */}
|
|
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}>
|
|
250
|
+
<react_native_1.Text style={[styles.actionBtnText, (isRecordingAudio || audioUri) ? { color: '#FFF' } : undefined]}>
|
|
251
|
+
{!audioRecordingAvailable
|
|
252
|
+
? '🎙 Audio Unavailable'
|
|
253
|
+
: isRecordingAudio
|
|
254
|
+
? `Stop (${audioDuration})`
|
|
255
|
+
: audioUri
|
|
256
|
+
? 'Re-record Audio'
|
|
257
|
+
: '🎙 Record Audio'}
|
|
258
|
+
</react_native_1.Text>
|
|
259
|
+
</react_native_1.TouchableOpacity>
|
|
260
|
+
|
|
261
|
+
{/* Screen Recorder */}
|
|
262
|
+
{onStartRecording && !videoUri && (<react_native_1.TouchableOpacity style={styles.actionBtn} onPress={onStartRecording}>
|
|
263
|
+
<react_native_1.Text style={styles.actionBtnText}>🎥 Record Screen</react_native_1.Text>
|
|
264
|
+
</react_native_1.TouchableOpacity>)}
|
|
265
|
+
</react_native_1.View>
|
|
266
|
+
|
|
267
|
+
<react_native_1.TouchableOpacity style={[styles.submitBtn, isSending && styles.disabledBtn]} onPress={sendFeedback} disabled={isSending}>
|
|
268
|
+
{isSending ? (<react_native_1.ActivityIndicator color="#FFF"/>) : (<react_native_1.Text style={styles.submitText}>Send Feedback</react_native_1.Text>)}
|
|
269
|
+
</react_native_1.TouchableOpacity>
|
|
270
|
+
</react_native_1.View>
|
|
271
|
+
</react_native_1.ScrollView>
|
|
272
|
+
</react_native_1.KeyboardAvoidingView>
|
|
273
|
+
</react_native_1.TouchableWithoutFeedback>
|
|
274
|
+
</react_native_1.Modal>);
|
|
275
|
+
};
|
|
276
|
+
exports.FeedbackModal = FeedbackModal;
|
|
277
|
+
const styles = react_native_1.StyleSheet.create({
|
|
278
|
+
container: {
|
|
279
|
+
flex: 1,
|
|
280
|
+
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
281
|
+
justifyContent: 'flex-end',
|
|
282
|
+
},
|
|
283
|
+
scrollContainer: {
|
|
284
|
+
flexGrow: 1,
|
|
285
|
+
justifyContent: 'flex-end',
|
|
286
|
+
},
|
|
287
|
+
card: {
|
|
288
|
+
backgroundColor: '#FFF',
|
|
289
|
+
borderTopLeftRadius: 20,
|
|
290
|
+
borderTopRightRadius: 20,
|
|
291
|
+
padding: 20,
|
|
292
|
+
paddingBottom: 40,
|
|
293
|
+
maxHeight: '90%',
|
|
294
|
+
},
|
|
295
|
+
header: {
|
|
296
|
+
flexDirection: 'row',
|
|
297
|
+
justifyContent: 'space-between',
|
|
298
|
+
alignItems: 'center',
|
|
299
|
+
marginBottom: 20,
|
|
300
|
+
},
|
|
301
|
+
title: {
|
|
302
|
+
fontSize: 20,
|
|
303
|
+
fontWeight: 'bold',
|
|
304
|
+
color: '#000',
|
|
305
|
+
},
|
|
306
|
+
closeBtn: {
|
|
307
|
+
fontSize: 24,
|
|
308
|
+
color: '#999',
|
|
309
|
+
padding: 5,
|
|
310
|
+
},
|
|
311
|
+
mediaRow: {
|
|
312
|
+
flexDirection: 'row',
|
|
313
|
+
marginBottom: 15,
|
|
314
|
+
},
|
|
315
|
+
mediaItem: {
|
|
316
|
+
width: 100,
|
|
317
|
+
height: 100,
|
|
318
|
+
borderRadius: 10,
|
|
319
|
+
overflow: 'hidden',
|
|
320
|
+
marginRight: 10,
|
|
321
|
+
position: 'relative',
|
|
322
|
+
},
|
|
323
|
+
mediaPreview: {
|
|
324
|
+
width: '100%',
|
|
325
|
+
height: '100%',
|
|
326
|
+
},
|
|
327
|
+
mediaBadge: {
|
|
328
|
+
position: 'absolute',
|
|
329
|
+
bottom: 0,
|
|
330
|
+
left: 0,
|
|
331
|
+
right: 0,
|
|
332
|
+
backgroundColor: 'rgba(0,0,0,0.6)',
|
|
333
|
+
padding: 4,
|
|
334
|
+
alignItems: 'center',
|
|
335
|
+
},
|
|
336
|
+
mediaBadgeText: {
|
|
337
|
+
color: '#FFF',
|
|
338
|
+
fontSize: 10,
|
|
339
|
+
fontWeight: 'bold',
|
|
340
|
+
},
|
|
341
|
+
input: {
|
|
342
|
+
backgroundColor: '#f9f9f9',
|
|
343
|
+
borderRadius: 10,
|
|
344
|
+
padding: 15,
|
|
345
|
+
height: 100,
|
|
346
|
+
textAlignVertical: 'top',
|
|
347
|
+
fontSize: 16,
|
|
348
|
+
color: '#000',
|
|
349
|
+
marginBottom: 15,
|
|
350
|
+
},
|
|
351
|
+
actionsRow: {
|
|
352
|
+
flexDirection: 'row',
|
|
353
|
+
justifyContent: 'space-between',
|
|
354
|
+
marginBottom: 20,
|
|
355
|
+
},
|
|
356
|
+
actionBtn: {
|
|
357
|
+
flex: 1,
|
|
358
|
+
backgroundColor: '#f0f0f0',
|
|
359
|
+
padding: 12,
|
|
360
|
+
borderRadius: 8,
|
|
361
|
+
alignItems: 'center',
|
|
362
|
+
marginHorizontal: 5,
|
|
363
|
+
},
|
|
364
|
+
recordingBtn: {
|
|
365
|
+
backgroundColor: '#ff4444',
|
|
366
|
+
},
|
|
367
|
+
hasAudioBtn: {
|
|
368
|
+
backgroundColor: '#4CAF50',
|
|
369
|
+
},
|
|
370
|
+
disabledActionBtn: {
|
|
371
|
+
opacity: 0.45,
|
|
372
|
+
},
|
|
373
|
+
actionBtnText: {
|
|
374
|
+
fontSize: 14,
|
|
375
|
+
fontWeight: '600',
|
|
376
|
+
color: '#333',
|
|
377
|
+
},
|
|
378
|
+
submitBtn: {
|
|
379
|
+
backgroundColor: '#000',
|
|
380
|
+
borderRadius: 12,
|
|
381
|
+
padding: 16,
|
|
382
|
+
alignItems: 'center',
|
|
383
|
+
marginTop: 10,
|
|
384
|
+
},
|
|
385
|
+
disabledBtn: {
|
|
386
|
+
opacity: 0.7,
|
|
387
|
+
},
|
|
388
|
+
submitText: {
|
|
389
|
+
color: '#FFF',
|
|
390
|
+
fontSize: 16,
|
|
391
|
+
fontWeight: 'bold',
|
|
392
|
+
},
|
|
393
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface IInstallWrapperProps {
|
|
3
|
+
apiKey: string;
|
|
4
|
+
apiEndpoint?: string;
|
|
5
|
+
children?: React.ReactNode;
|
|
6
|
+
enabled?: boolean;
|
|
7
|
+
showDebugButton?: boolean;
|
|
8
|
+
showFloatingButtonOnEmulator?: boolean;
|
|
9
|
+
floatingButtonLabel?: string;
|
|
10
|
+
pushToken?: string;
|
|
11
|
+
autoRegisterPushToken?: boolean;
|
|
12
|
+
projectId?: string;
|
|
13
|
+
onPushTokenRegisterError?: (error: string) => void;
|
|
14
|
+
}
|
|
15
|
+
export declare const IInstallWrapper: React.FC<IInstallWrapperProps>;
|
|
16
|
+
export default IInstallWrapper;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.IInstallWrapper = void 0;
|
|
7
|
+
const react_1 = __importDefault(require("react"));
|
|
8
|
+
const index_1 = require("./index");
|
|
9
|
+
// Backward-compatible wrapper. The core IInstall component now handles:
|
|
10
|
+
// - shake gesture on real devices
|
|
11
|
+
// - floating manual trigger button on simulator/emulator
|
|
12
|
+
const IInstallWrapper = ({ apiKey, apiEndpoint = 'https://iinstall.app', children, enabled = true, showDebugButton: _showDebugButton = false, showFloatingButtonOnEmulator = true, floatingButtonLabel = 'Report Issue', pushToken, autoRegisterPushToken = true, projectId, onPushTokenRegisterError, }) => {
|
|
13
|
+
return (<index_1.IInstall apiKey={apiKey} apiEndpoint={apiEndpoint} enabled={enabled} showFloatingButtonOnEmulator={showFloatingButtonOnEmulator} floatingButtonLabel={floatingButtonLabel} pushToken={pushToken} autoRegisterPushToken={autoRegisterPushToken} projectId={projectId} onPushTokenRegisterError={onPushTokenRegisterError}>
|
|
14
|
+
{children}
|
|
15
|
+
</index_1.IInstall>);
|
|
16
|
+
};
|
|
17
|
+
exports.IInstallWrapper = IInstallWrapper;
|
|
18
|
+
exports.default = exports.IInstallWrapper;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ShakeDetector = void 0;
|
|
4
|
+
/* eslint-disable */
|
|
5
|
+
const operators_1 = require("rxjs/operators");
|
|
6
|
+
// Safe import for react-native-sensors to prevent crashes if native module is missing
|
|
7
|
+
let accelerometer;
|
|
8
|
+
let setUpdateIntervalForType;
|
|
9
|
+
let SensorTypes;
|
|
10
|
+
try {
|
|
11
|
+
const sensors = require('react-native-sensors');
|
|
12
|
+
accelerometer = sensors.accelerometer;
|
|
13
|
+
setUpdateIntervalForType = sensors.setUpdateIntervalForType;
|
|
14
|
+
SensorTypes = sensors.SensorTypes;
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
console.warn('[iInstall SDK] Shake detection disabled: react-native-sensors native module not found. Please ensure the native module is linked and the app is rebuilt.');
|
|
18
|
+
}
|
|
19
|
+
const SHAKE_THRESHOLD = 2.5; // g-force
|
|
20
|
+
const MIN_TIME_BETWEEN_SHAKES = 1000; // ms
|
|
21
|
+
class ShakeDetector {
|
|
22
|
+
subscription = null;
|
|
23
|
+
lastShakeTime = 0;
|
|
24
|
+
onShake;
|
|
25
|
+
constructor(onShake) {
|
|
26
|
+
this.onShake = onShake;
|
|
27
|
+
if (setUpdateIntervalForType && SensorTypes) {
|
|
28
|
+
try {
|
|
29
|
+
setUpdateIntervalForType(SensorTypes.accelerometer, 100); // 100ms interval
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
console.warn('[iInstall SDK] Failed to set accelerometer update interval:', e);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
start() {
|
|
37
|
+
if (this.subscription)
|
|
38
|
+
return;
|
|
39
|
+
if (!accelerometer) {
|
|
40
|
+
// Don't spam logs, just return silently or maybe log once if debugging
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
this.subscription = accelerometer
|
|
45
|
+
.pipe((0, operators_1.map)(({ x, y, z }) => Math.sqrt(x * x + y * y + z * z)), (0, operators_1.filter)((g) => g > SHAKE_THRESHOLD))
|
|
46
|
+
.subscribe(() => {
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
if (now - this.lastShakeTime > MIN_TIME_BETWEEN_SHAKES) {
|
|
49
|
+
this.lastShakeTime = now;
|
|
50
|
+
console.log('TesterFlow: Shake detected!');
|
|
51
|
+
this.onShake();
|
|
52
|
+
}
|
|
53
|
+
}, (error) => {
|
|
54
|
+
console.warn('[iInstall SDK] Error in shake detection subscription:', error);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
console.warn('[iInstall SDK] Failed to start shake detection:', e);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
stop() {
|
|
62
|
+
if (this.subscription) {
|
|
63
|
+
this.subscription.unsubscribe();
|
|
64
|
+
this.subscription = null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
exports.ShakeDetector = ShakeDetector;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface IInstallProps {
|
|
3
|
+
apiKey: string;
|
|
4
|
+
apiEndpoint?: string;
|
|
5
|
+
children?: React.ReactNode;
|
|
6
|
+
enabled?: boolean;
|
|
7
|
+
showFloatingButtonOnEmulator?: boolean;
|
|
8
|
+
floatingButtonLabel?: string;
|
|
9
|
+
pushToken?: string;
|
|
10
|
+
autoRegisterPushToken?: boolean;
|
|
11
|
+
projectId?: string;
|
|
12
|
+
onPushTokenRegisterError?: (error: string) => void;
|
|
13
|
+
}
|
|
14
|
+
export declare const IInstall: React.FC<IInstallProps>;
|
|
15
|
+
export default IInstall;
|
|
16
|
+
export { IInstallWrapper } from './IInstallWrapper';
|
|
17
|
+
export { registerPushToken, unregisterPushToken, type RegisterPushTokenParams, type UnregisterPushTokenParams, type PushRegistrationResult, type PushPlatform, } from './pushRegistration';
|