react-native-iinstall 0.1.0

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 ADDED
@@ -0,0 +1,65 @@
1
+ # IInstall SDK for React Native
2
+
3
+ The official React Native SDK for [IInstall](https://iinstall.app). Enable "Shake to Report" in your app to gather instant feedback from testers.
4
+
5
+ ## Installation
6
+
7
+ 1. Install the SDK:
8
+ ```bash
9
+ npm install react-native-iinstall
10
+ ```
11
+
12
+ **Note**: This package automatically installs required dependencies (`react-native-sensors`, `react-native-view-shot`, etc.).
13
+
14
+ 2. Link native modules (iOS only, non-Expo):
15
+ ```bash
16
+ cd ios && pod install
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ Wrap your main app component with the `<IInstall>` provider.
22
+
23
+ ```tsx
24
+ import React from 'react';
25
+ import { IInstall } from 'react-native-iinstall';
26
+ import AppNavigation from './src/AppNavigation';
27
+
28
+ const App = () => {
29
+ return (
30
+ // Get your API Key from the IInstall Dashboard (Project Settings)
31
+ <IInstall
32
+ apiKey="YOUR_PROJECT_API_KEY"
33
+ apiEndpoint="https://iinstall.app" // Optional, defaults to production
34
+ enabled={__DEV__} // Optional: Only enable in dev/test builds
35
+ >
36
+ <AppNavigation />
37
+ </IInstall>
38
+ );
39
+ };
40
+
41
+ export default App;
42
+ ```
43
+
44
+ ## Features
45
+
46
+ - **Shake Detection**: Automatically detects when a user shakes the device.
47
+ - **Screenshot Capture**: Captures the current screen state instantly.
48
+ - **Metadata Collection**: Automatically gathers device model, OS version, and app build number.
49
+ - **Feedback Submission**: Uploads reports directly to your TesterFlow dashboard.
50
+
51
+ ## Permissions
52
+
53
+ ### Android
54
+ Add to `AndroidManifest.xml`:
55
+ ```xml
56
+ <uses-permission android:name="android.permission.INTERNET" />
57
+ ```
58
+
59
+ ### iOS
60
+ Add to `Info.plist` (if required by your specific usage, usually standard permissions suffice for screenshots/network).
61
+
62
+ ## Troubleshooting
63
+
64
+ - **Shake not working?** Ensure you are testing on a real device or enabling "Shake" in the simulator menu.
65
+ - **Network errors?** Check if `apiEndpoint` is reachable and `apiKey` is correct.
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ interface FeedbackModalProps {
3
+ visible: boolean;
4
+ onClose: () => void;
5
+ screenshotUri: string | null;
6
+ apiKey: string;
7
+ apiEndpoint: string;
8
+ }
9
+ export declare const FeedbackModal: React.FC<FeedbackModalProps>;
10
+ export {};
@@ -0,0 +1,209 @@
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
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.FeedbackModal = void 0;
40
+ const react_1 = __importStar(require("react"));
41
+ const react_native_1 = require("react-native");
42
+ const react_native_device_info_1 = __importDefault(require("react-native-device-info"));
43
+ const FeedbackModal = ({ visible, onClose, screenshotUri, apiKey, apiEndpoint }) => {
44
+ const [description, setDescription] = (0, react_1.useState)('');
45
+ const [isSending, setIsSending] = (0, react_1.useState)(false);
46
+ const sendFeedback = async () => {
47
+ if (!description.trim()) {
48
+ react_native_1.Alert.alert('Please describe the issue');
49
+ return;
50
+ }
51
+ setIsSending(true);
52
+ try {
53
+ const formData = new FormData();
54
+ // 1. Screenshot
55
+ if (screenshotUri) {
56
+ const filename = screenshotUri.split('/').pop();
57
+ const match = /\.(\w+)$/.exec(filename || '');
58
+ const type = match ? `image/${match[1]}` : `image`;
59
+ formData.append('screenshot', {
60
+ uri: react_native_1.Platform.OS === 'ios' ? screenshotUri.replace('file://', '') : screenshotUri,
61
+ name: filename || 'screenshot.png',
62
+ type,
63
+ });
64
+ }
65
+ // 2. Metadata
66
+ const metadata = {
67
+ device: react_native_device_info_1.default.getModel(),
68
+ systemName: react_native_device_info_1.default.getSystemName(),
69
+ systemVersion: react_native_device_info_1.default.getSystemVersion(),
70
+ appVersion: react_native_device_info_1.default.getVersion(),
71
+ buildNumber: react_native_device_info_1.default.getBuildNumber(),
72
+ brand: react_native_device_info_1.default.getBrand(),
73
+ isEmulator: await react_native_device_info_1.default.isEmulator(),
74
+ };
75
+ formData.append('metadata', JSON.stringify(metadata));
76
+ // 3. Other fields
77
+ formData.append('description', description);
78
+ formData.append('apiKey', apiKey);
79
+ formData.append('udid', await react_native_device_info_1.default.getUniqueId()); // Best effort UDID/ID
80
+ // 4. Send
81
+ const response = await fetch(`${apiEndpoint}/api/feedback`, {
82
+ method: 'POST',
83
+ headers: {
84
+ 'Content-Type': 'multipart/form-data',
85
+ },
86
+ body: formData,
87
+ });
88
+ const result = await response.json();
89
+ if (response.ok) {
90
+ react_native_1.Alert.alert('Success', 'Feedback sent successfully!');
91
+ setDescription('');
92
+ onClose();
93
+ }
94
+ else {
95
+ throw new Error(result.error || 'Failed to send');
96
+ }
97
+ }
98
+ catch (error) {
99
+ console.error('TesterFlow Error:', error);
100
+ react_native_1.Alert.alert('Error', 'Failed to send feedback. Please try again.');
101
+ }
102
+ finally {
103
+ setIsSending(false);
104
+ }
105
+ };
106
+ if (!visible)
107
+ return null;
108
+ return (<react_native_1.Modal visible={visible} transparent animationType="slide" onRequestClose={onClose}>
109
+ <react_native_1.KeyboardAvoidingView behavior={react_native_1.Platform.OS === 'ios' ? 'padding' : 'height'} style={styles.container}>
110
+ <react_native_1.View style={styles.card}>
111
+ <react_native_1.View style={styles.header}>
112
+ <react_native_1.Text style={styles.title}>Report an Issue</react_native_1.Text>
113
+ <react_native_1.TouchableOpacity onPress={onClose}>
114
+ <react_native_1.Text style={styles.closeBtn}>✕</react_native_1.Text>
115
+ </react_native_1.TouchableOpacity>
116
+ </react_native_1.View>
117
+
118
+ {screenshotUri && (<react_native_1.View style={styles.previewContainer}>
119
+ <react_native_1.Image source={{ uri: screenshotUri }} style={styles.preview} resizeMode="contain"/>
120
+ <react_native_1.Text style={styles.previewLabel}>Screenshot Attached</react_native_1.Text>
121
+ </react_native_1.View>)}
122
+
123
+ <react_native_1.TextInput style={styles.input} placeholder="What happened? Describe the bug..." placeholderTextColor="#999" multiline value={description} onChangeText={setDescription} autoFocus/>
124
+
125
+ <react_native_1.TouchableOpacity style={[styles.submitBtn, isSending && styles.disabledBtn]} onPress={sendFeedback} disabled={isSending}>
126
+ {isSending ? (<react_native_1.ActivityIndicator color="#FFF"/>) : (<react_native_1.Text style={styles.submitText}>Send Feedback</react_native_1.Text>)}
127
+ </react_native_1.TouchableOpacity>
128
+ </react_native_1.View>
129
+ </react_native_1.KeyboardAvoidingView>
130
+ </react_native_1.Modal>);
131
+ };
132
+ exports.FeedbackModal = FeedbackModal;
133
+ const styles = react_native_1.StyleSheet.create({
134
+ container: {
135
+ flex: 1,
136
+ backgroundColor: 'rgba(0,0,0,0.5)',
137
+ justifyContent: 'flex-end',
138
+ },
139
+ card: {
140
+ backgroundColor: '#FFF',
141
+ borderTopLeftRadius: 20,
142
+ borderTopRightRadius: 20,
143
+ padding: 20,
144
+ minHeight: 400,
145
+ },
146
+ header: {
147
+ flexDirection: 'row',
148
+ justifyContent: 'space-between',
149
+ alignItems: 'center',
150
+ marginBottom: 20,
151
+ },
152
+ title: {
153
+ fontSize: 20,
154
+ fontWeight: 'bold',
155
+ color: '#000',
156
+ },
157
+ closeBtn: {
158
+ fontSize: 24,
159
+ color: '#999',
160
+ padding: 5,
161
+ },
162
+ previewContainer: {
163
+ height: 150,
164
+ backgroundColor: '#f0f0f0',
165
+ borderRadius: 10,
166
+ marginBottom: 15,
167
+ justifyContent: 'center',
168
+ alignItems: 'center',
169
+ overflow: 'hidden',
170
+ },
171
+ preview: {
172
+ width: '100%',
173
+ height: '100%',
174
+ },
175
+ previewLabel: {
176
+ position: 'absolute',
177
+ bottom: 5,
178
+ right: 5,
179
+ backgroundColor: 'rgba(0,0,0,0.6)',
180
+ color: '#FFF',
181
+ fontSize: 10,
182
+ padding: 4,
183
+ borderRadius: 4,
184
+ },
185
+ input: {
186
+ backgroundColor: '#f9f9f9',
187
+ borderRadius: 10,
188
+ padding: 15,
189
+ height: 120,
190
+ textAlignVertical: 'top',
191
+ fontSize: 16,
192
+ color: '#000',
193
+ marginBottom: 20,
194
+ },
195
+ submitBtn: {
196
+ backgroundColor: '#007AFF',
197
+ padding: 15,
198
+ borderRadius: 12,
199
+ alignItems: 'center',
200
+ },
201
+ disabledBtn: {
202
+ backgroundColor: '#ccc',
203
+ },
204
+ submitText: {
205
+ color: '#FFF',
206
+ fontSize: 16,
207
+ fontWeight: 'bold',
208
+ },
209
+ });
@@ -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,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ShakeDetector = void 0;
4
+ const react_native_sensors_1 = require("react-native-sensors");
5
+ const operators_1 = require("rxjs/operators");
6
+ const SHAKE_THRESHOLD = 2.5; // g-force
7
+ const MIN_TIME_BETWEEN_SHAKES = 1000; // ms
8
+ class ShakeDetector {
9
+ subscription = null;
10
+ lastShakeTime = 0;
11
+ onShake;
12
+ constructor(onShake) {
13
+ this.onShake = onShake;
14
+ (0, react_native_sensors_1.setUpdateIntervalForType)(react_native_sensors_1.SensorTypes.accelerometer, 100); // 100ms interval
15
+ }
16
+ start() {
17
+ if (this.subscription)
18
+ return;
19
+ this.subscription = react_native_sensors_1.accelerometer
20
+ .pipe((0, operators_1.map)(({ x, y, z }) => Math.sqrt(x * x + y * y + z * z)), (0, operators_1.filter)(g => g > SHAKE_THRESHOLD))
21
+ .subscribe(() => {
22
+ const now = Date.now();
23
+ if (now - this.lastShakeTime > MIN_TIME_BETWEEN_SHAKES) {
24
+ this.lastShakeTime = now;
25
+ console.log('TesterFlow: Shake detected!');
26
+ this.onShake();
27
+ }
28
+ });
29
+ }
30
+ stop() {
31
+ if (this.subscription) {
32
+ this.subscription.unsubscribe();
33
+ this.subscription = null;
34
+ }
35
+ }
36
+ }
37
+ exports.ShakeDetector = ShakeDetector;
package/lib/index.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ import React from 'react';
2
+ interface IInstallProps {
3
+ apiKey: string;
4
+ apiEndpoint?: string;
5
+ children?: React.ReactNode;
6
+ enabled?: boolean;
7
+ }
8
+ export declare const IInstall: React.FC<IInstallProps>;
9
+ export default IInstall;
package/lib/index.js ADDED
@@ -0,0 +1,80 @@
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.IInstall = void 0;
37
+ const react_1 = __importStar(require("react"));
38
+ const react_native_1 = require("react-native");
39
+ const react_native_view_shot_1 = require("react-native-view-shot");
40
+ const ShakeDetector_1 = require("./ShakeDetector");
41
+ const FeedbackModal_1 = require("./FeedbackModal");
42
+ const IInstall = ({ apiKey, apiEndpoint = 'https://iinstall.app', children, enabled = true }) => {
43
+ const [modalVisible, setModalVisible] = (0, react_1.useState)(false);
44
+ const [screenshotUri, setScreenshotUri] = (0, react_1.useState)(null);
45
+ const shakeDetectorRef = (0, react_1.useRef)(null);
46
+ (0, react_1.useEffect)(() => {
47
+ if (!enabled)
48
+ return;
49
+ shakeDetectorRef.current = new ShakeDetector_1.ShakeDetector(handleShake);
50
+ shakeDetectorRef.current.start();
51
+ return () => {
52
+ shakeDetectorRef.current?.stop();
53
+ };
54
+ }, [enabled]);
55
+ const handleShake = async () => {
56
+ if (modalVisible)
57
+ return; // Already open
58
+ try {
59
+ // Capture screenshot
60
+ const uri = await (0, react_native_view_shot_1.captureScreen)({
61
+ format: 'png',
62
+ quality: 0.8,
63
+ });
64
+ setScreenshotUri(uri);
65
+ setModalVisible(true);
66
+ }
67
+ catch (error) {
68
+ console.error('IInstall: Failed to capture screenshot', error);
69
+ // Still show modal without screenshot
70
+ setScreenshotUri(null);
71
+ setModalVisible(true);
72
+ }
73
+ };
74
+ return (<react_native_1.View style={react_native_1.StyleSheet.absoluteFill}>
75
+ {children}
76
+ <FeedbackModal_1.FeedbackModal visible={modalVisible} onClose={() => setModalVisible(false)} screenshotUri={screenshotUri} apiKey={apiKey} apiEndpoint={apiEndpoint}/>
77
+ </react_native_1.View>);
78
+ };
79
+ exports.IInstall = IInstall;
80
+ exports.default = exports.IInstall;
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "react-native-iinstall",
3
+ "version": "0.1.0",
4
+ "description": "IInstall SDK for React Native - Shake to Report",
5
+ "author": "TesterFlow Team",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/your-username/iinstall-sdk.git"
10
+ },
11
+ "keywords": [
12
+ "react-native",
13
+ "feedback",
14
+ "bug-report",
15
+ "iinstall",
16
+ "shake"
17
+ ],
18
+ "main": "lib/index.js",
19
+ "types": "lib/index.d.ts",
20
+ "files": [
21
+ "lib",
22
+ "src"
23
+ ],
24
+ "scripts": {
25
+ "build": "tsc",
26
+ "prepare": "npm run build"
27
+ },
28
+ "peerDependencies": {
29
+ "react": ">=16.8.0",
30
+ "react-native": ">=0.60.0"
31
+ },
32
+ "dependencies": {
33
+ "react-native-sensors": "^7.3.0",
34
+ "react-native-view-shot": "^3.1.2",
35
+ "react-native-device-info": "^10.0.0",
36
+ "rxjs": "^7.0.0"
37
+ },
38
+ "devDependencies": {
39
+ "@types/react": "^18.0.0",
40
+ "@types/react-native": "^0.70.0",
41
+ "react": "18.2.0",
42
+ "react-native": "0.72.6",
43
+ "typescript": "^5.0.0"
44
+ }
45
+ }
@@ -0,0 +1,229 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import {
3
+ Modal,
4
+ View,
5
+ Text,
6
+ TextInput,
7
+ TouchableOpacity,
8
+ StyleSheet,
9
+ ActivityIndicator,
10
+ Image,
11
+ Alert,
12
+ Platform,
13
+ KeyboardAvoidingView
14
+ } from 'react-native';
15
+ import DeviceInfo from 'react-native-device-info';
16
+
17
+ interface FeedbackModalProps {
18
+ visible: boolean;
19
+ onClose: () => void;
20
+ screenshotUri: string | null;
21
+ apiKey: string;
22
+ apiEndpoint: string;
23
+ }
24
+
25
+ export const FeedbackModal: React.FC<FeedbackModalProps> = ({
26
+ visible,
27
+ onClose,
28
+ screenshotUri,
29
+ apiKey,
30
+ apiEndpoint
31
+ }) => {
32
+ const [description, setDescription] = useState('');
33
+ const [isSending, setIsSending] = useState(false);
34
+
35
+ const sendFeedback = async () => {
36
+ if (!description.trim()) {
37
+ Alert.alert('Please describe the issue');
38
+ return;
39
+ }
40
+
41
+ setIsSending(true);
42
+
43
+ try {
44
+ const formData = new FormData();
45
+
46
+ // 1. Screenshot
47
+ if (screenshotUri) {
48
+ const filename = screenshotUri.split('/').pop();
49
+ const match = /\.(\w+)$/.exec(filename || '');
50
+ const type = match ? `image/${match[1]}` : `image`;
51
+
52
+ formData.append('screenshot', {
53
+ uri: Platform.OS === 'ios' ? screenshotUri.replace('file://', '') : screenshotUri,
54
+ name: filename || 'screenshot.png',
55
+ type,
56
+ } as any);
57
+ }
58
+
59
+ // 2. Metadata
60
+ const metadata = {
61
+ device: DeviceInfo.getModel(),
62
+ systemName: DeviceInfo.getSystemName(),
63
+ systemVersion: DeviceInfo.getSystemVersion(),
64
+ appVersion: DeviceInfo.getVersion(),
65
+ buildNumber: DeviceInfo.getBuildNumber(),
66
+ brand: DeviceInfo.getBrand(),
67
+ isEmulator: await DeviceInfo.isEmulator(),
68
+ };
69
+ formData.append('metadata', JSON.stringify(metadata));
70
+
71
+ // 3. Other fields
72
+ formData.append('description', description);
73
+ formData.append('apiKey', apiKey);
74
+ formData.append('udid', await DeviceInfo.getUniqueId()); // Best effort UDID/ID
75
+
76
+ // 4. Send
77
+ const response = await fetch(`${apiEndpoint}/api/feedback`, {
78
+ method: 'POST',
79
+ headers: {
80
+ 'Content-Type': 'multipart/form-data',
81
+ },
82
+ body: formData,
83
+ });
84
+
85
+ const result = await response.json();
86
+
87
+ if (response.ok) {
88
+ Alert.alert('Success', 'Feedback sent successfully!');
89
+ setDescription('');
90
+ onClose();
91
+ } else {
92
+ throw new Error(result.error || 'Failed to send');
93
+ }
94
+
95
+ } catch (error) {
96
+ console.error('TesterFlow Error:', error);
97
+ Alert.alert('Error', 'Failed to send feedback. Please try again.');
98
+ } finally {
99
+ setIsSending(false);
100
+ }
101
+ };
102
+
103
+ if (!visible) return null;
104
+
105
+ return (
106
+ <Modal visible={visible} transparent animationType="slide" onRequestClose={onClose}>
107
+ <KeyboardAvoidingView
108
+ behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
109
+ style={styles.container}
110
+ >
111
+ <View style={styles.card}>
112
+ <View style={styles.header}>
113
+ <Text style={styles.title}>Report an Issue</Text>
114
+ <TouchableOpacity onPress={onClose}>
115
+ <Text style={styles.closeBtn}>✕</Text>
116
+ </TouchableOpacity>
117
+ </View>
118
+
119
+ {screenshotUri && (
120
+ <View style={styles.previewContainer}>
121
+ <Image source={{ uri: screenshotUri }} style={styles.preview} resizeMode="contain" />
122
+ <Text style={styles.previewLabel}>Screenshot Attached</Text>
123
+ </View>
124
+ )}
125
+
126
+ <TextInput
127
+ style={styles.input}
128
+ placeholder="What happened? Describe the bug..."
129
+ placeholderTextColor="#999"
130
+ multiline
131
+ value={description}
132
+ onChangeText={setDescription}
133
+ autoFocus
134
+ />
135
+
136
+ <TouchableOpacity
137
+ style={[styles.submitBtn, isSending && styles.disabledBtn]}
138
+ onPress={sendFeedback}
139
+ disabled={isSending}
140
+ >
141
+ {isSending ? (
142
+ <ActivityIndicator color="#FFF" />
143
+ ) : (
144
+ <Text style={styles.submitText}>Send Feedback</Text>
145
+ )}
146
+ </TouchableOpacity>
147
+ </View>
148
+ </KeyboardAvoidingView>
149
+ </Modal>
150
+ );
151
+ };
152
+
153
+ const styles = StyleSheet.create({
154
+ container: {
155
+ flex: 1,
156
+ backgroundColor: 'rgba(0,0,0,0.5)',
157
+ justifyContent: 'flex-end',
158
+ },
159
+ card: {
160
+ backgroundColor: '#FFF',
161
+ borderTopLeftRadius: 20,
162
+ borderTopRightRadius: 20,
163
+ padding: 20,
164
+ minHeight: 400,
165
+ },
166
+ header: {
167
+ flexDirection: 'row',
168
+ justifyContent: 'space-between',
169
+ alignItems: 'center',
170
+ marginBottom: 20,
171
+ },
172
+ title: {
173
+ fontSize: 20,
174
+ fontWeight: 'bold',
175
+ color: '#000',
176
+ },
177
+ closeBtn: {
178
+ fontSize: 24,
179
+ color: '#999',
180
+ padding: 5,
181
+ },
182
+ previewContainer: {
183
+ height: 150,
184
+ backgroundColor: '#f0f0f0',
185
+ borderRadius: 10,
186
+ marginBottom: 15,
187
+ justifyContent: 'center',
188
+ alignItems: 'center',
189
+ overflow: 'hidden',
190
+ },
191
+ preview: {
192
+ width: '100%',
193
+ height: '100%',
194
+ },
195
+ previewLabel: {
196
+ position: 'absolute',
197
+ bottom: 5,
198
+ right: 5,
199
+ backgroundColor: 'rgba(0,0,0,0.6)',
200
+ color: '#FFF',
201
+ fontSize: 10,
202
+ padding: 4,
203
+ borderRadius: 4,
204
+ },
205
+ input: {
206
+ backgroundColor: '#f9f9f9',
207
+ borderRadius: 10,
208
+ padding: 15,
209
+ height: 120,
210
+ textAlignVertical: 'top',
211
+ fontSize: 16,
212
+ color: '#000',
213
+ marginBottom: 20,
214
+ },
215
+ submitBtn: {
216
+ backgroundColor: '#007AFF',
217
+ padding: 15,
218
+ borderRadius: 12,
219
+ alignItems: 'center',
220
+ },
221
+ disabledBtn: {
222
+ backgroundColor: '#ccc',
223
+ },
224
+ submitText: {
225
+ color: '#FFF',
226
+ fontSize: 16,
227
+ fontWeight: 'bold',
228
+ },
229
+ });
@@ -0,0 +1,41 @@
1
+ import { accelerometer, setUpdateIntervalForType, SensorTypes } from 'react-native-sensors';
2
+ import { map, filter } from 'rxjs/operators';
3
+
4
+ const SHAKE_THRESHOLD = 2.5; // g-force
5
+ const MIN_TIME_BETWEEN_SHAKES = 1000; // ms
6
+
7
+ export class ShakeDetector {
8
+ private subscription: any = null;
9
+ private lastShakeTime = 0;
10
+ private onShake: () => void;
11
+
12
+ constructor(onShake: () => void) {
13
+ this.onShake = onShake;
14
+ setUpdateIntervalForType(SensorTypes.accelerometer, 100); // 100ms interval
15
+ }
16
+
17
+ start() {
18
+ if (this.subscription) return;
19
+
20
+ this.subscription = accelerometer
21
+ .pipe(
22
+ map(({ x, y, z }) => Math.sqrt(x * x + y * y + z * z)),
23
+ filter(g => g > SHAKE_THRESHOLD)
24
+ )
25
+ .subscribe(() => {
26
+ const now = Date.now();
27
+ if (now - this.lastShakeTime > MIN_TIME_BETWEEN_SHAKES) {
28
+ this.lastShakeTime = now;
29
+ console.log('TesterFlow: Shake detected!');
30
+ this.onShake();
31
+ }
32
+ });
33
+ }
34
+
35
+ stop() {
36
+ if (this.subscription) {
37
+ this.subscription.unsubscribe();
38
+ this.subscription = null;
39
+ }
40
+ }
41
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,68 @@
1
+ import React, { useEffect, useState, useRef } from 'react';
2
+ import { View, StyleSheet } from 'react-native';
3
+ import { captureScreen } from 'react-native-view-shot';
4
+ import { ShakeDetector } from './ShakeDetector';
5
+ import { FeedbackModal } from './FeedbackModal';
6
+
7
+ interface IInstallProps {
8
+ apiKey: string;
9
+ apiEndpoint?: string; // e.g. https://iinstall.app
10
+ children?: React.ReactNode;
11
+ enabled?: boolean;
12
+ }
13
+
14
+ export const IInstall: React.FC<IInstallProps> = ({
15
+ apiKey,
16
+ apiEndpoint = 'https://iinstall.app',
17
+ children,
18
+ enabled = true
19
+ }) => {
20
+ const [modalVisible, setModalVisible] = useState(false);
21
+ const [screenshotUri, setScreenshotUri] = useState<string | null>(null);
22
+ const shakeDetectorRef = useRef<ShakeDetector | null>(null);
23
+
24
+ useEffect(() => {
25
+ if (!enabled) return;
26
+
27
+ shakeDetectorRef.current = new ShakeDetector(handleShake);
28
+ shakeDetectorRef.current.start();
29
+
30
+ return () => {
31
+ shakeDetectorRef.current?.stop();
32
+ };
33
+ }, [enabled]);
34
+
35
+ const handleShake = async () => {
36
+ if (modalVisible) return; // Already open
37
+
38
+ try {
39
+ // Capture screenshot
40
+ const uri = await captureScreen({
41
+ format: 'png',
42
+ quality: 0.8,
43
+ });
44
+ setScreenshotUri(uri);
45
+ setModalVisible(true);
46
+ } catch (error) {
47
+ console.error('IInstall: Failed to capture screenshot', error);
48
+ // Still show modal without screenshot
49
+ setScreenshotUri(null);
50
+ setModalVisible(true);
51
+ }
52
+ };
53
+
54
+ return (
55
+ <View style={StyleSheet.absoluteFill}>
56
+ {children}
57
+ <FeedbackModal
58
+ visible={modalVisible}
59
+ onClose={() => setModalVisible(false)}
60
+ screenshotUri={screenshotUri}
61
+ apiKey={apiKey}
62
+ apiEndpoint={apiEndpoint}
63
+ />
64
+ </View>
65
+ );
66
+ };
67
+
68
+ export default IInstall;