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 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
  ```
@@ -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,8 @@
1
+ {
2
+ "version": "0.2.14",
3
+ "highlights": [
4
+ "Auto push token registration support via IInstall pushToken prop",
5
+ "Manual helpers: registerPushToken and unregisterPushToken",
6
+ "Maintains audio/video feedback workflow and emulator floating button"
7
+ ]
8
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -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,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -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,8 @@
1
+ export declare class ShakeDetector {
2
+ private subscription;
3
+ private lastShakeTime;
4
+ private onShake;
5
+ constructor(onShake: () => void);
6
+ start(): void;
7
+ stop(): void;
8
+ }
@@ -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';