react-native-iinstall 0.2.3 → 0.2.8
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 +187 -0
- package/README.md +98 -10
- package/lib/FeedbackModal.js +54 -12
- package/lib/index.js +58 -5
- package/package.json +19 -4
- package/src/FeedbackModal.tsx +67 -12
- package/src/index.tsx +84 -5
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# IInstall Integration Guide
|
|
2
|
+
|
|
3
|
+
This guide explains how to integrate the IInstall SDK into your React Native application and clarifies the workflow for API keys and installer generation.
|
|
4
|
+
|
|
5
|
+
## 🚀 The Integration Workflow (Read This First)
|
|
6
|
+
|
|
7
|
+
There is a common "Chicken and Egg" confusion about when the API Key is created. Here is the correct order of operations:
|
|
8
|
+
|
|
9
|
+
1. **Create Project (Dashboard)**: You create a project in the IInstall Dashboard *first*.
|
|
10
|
+
* **Result**: The Dashboard generates a unique **API Key** for your project immediately.
|
|
11
|
+
2. **Integrate SDK (Code)**: You copy this API Key and add it to your React Native app's source code.
|
|
12
|
+
3. **Generate Installer (Build)**: You build your app (APK/IPA). The API Key is now "baked into" the app.
|
|
13
|
+
4. **Upload Build**: You upload the generated installer file to the Dashboard.
|
|
14
|
+
|
|
15
|
+
**Key Takeaway**: The API Key exists **BEFORE** you generate the installer. You do not need to upload a build to get a key.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 1. Requirements & Compatibility
|
|
20
|
+
|
|
21
|
+
Before starting, ensure your project meets these minimum requirements:
|
|
22
|
+
- **React Native**: Version 0.60.0 or higher (for auto-linking support).
|
|
23
|
+
- **iOS**: Version 11.0 or higher.
|
|
24
|
+
- **Android**: Version 5.0 (API Level 21) or higher.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 2. Get Your API Key
|
|
29
|
+
|
|
30
|
+
1. Log in to your IInstall Dashboard.
|
|
31
|
+
2. Click **"+ New Project"** (or use the empty state button).
|
|
32
|
+
3. Enter your App Name and Platform (iOS/Android).
|
|
33
|
+
4. Once created, you will be redirected to the **Project Dashboard**.
|
|
34
|
+
5. Look for the **"API Integration"** card (usually on the right side or bottom of the stats grid).
|
|
35
|
+
6. Copy the **Project API Key** (it looks like a long string of random characters).
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## 3. Install the SDK in React Native
|
|
40
|
+
|
|
41
|
+
In your React Native project directory, run:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install react-native-iinstall
|
|
45
|
+
# OR
|
|
46
|
+
yarn add react-native-iinstall
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Note**: The SDK automatically includes necessary dependencies like `react-native-sensors` and `react-native-view-shot`.
|
|
50
|
+
|
|
51
|
+
### Updating to Latest Version (v0.2.7)
|
|
52
|
+
|
|
53
|
+
If you're using an older version, update to get the latest audio/video improvements:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npm install react-native-iinstall@0.2.7
|
|
57
|
+
# OR
|
|
58
|
+
yarn add react-native-iinstall@0.2.7
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
**Key improvements in v0.2.7:**
|
|
62
|
+
- Fixed audio codec compatibility (AAC instead of ALAC)
|
|
63
|
+
- Enhanced screen recording reliability
|
|
64
|
+
- Improved modal state management
|
|
65
|
+
|
|
66
|
+
**iOS Specific Step:**
|
|
67
|
+
If you are on iOS and not using Expo Go, remember to install pods:
|
|
68
|
+
```bash
|
|
69
|
+
cd ios && pod install
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## 4. Configure the SDK
|
|
75
|
+
|
|
76
|
+
Open your root component file (usually `App.tsx` or `App.js`). Wrap your main application with the `<IInstall>` provider.
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
import React from 'react';
|
|
80
|
+
import { IInstall } from 'react-native-iinstall';
|
|
81
|
+
import AppNavigation from './src/AppNavigation'; // Your main app component
|
|
82
|
+
|
|
83
|
+
const App = () => {
|
|
84
|
+
return (
|
|
85
|
+
// PASTE YOUR API KEY HERE
|
|
86
|
+
<IInstall
|
|
87
|
+
apiKey="YOUR_COPIED_API_KEY_FROM_STEP_1"
|
|
88
|
+
apiEndpoint="https://iinstall.app"
|
|
89
|
+
enabled={true} // Set to false in production if desired
|
|
90
|
+
>
|
|
91
|
+
<AppNavigation />
|
|
92
|
+
</IInstall>
|
|
93
|
+
);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export default App;
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## 5. Audio & Video Feedback (v0.2.7+)
|
|
102
|
+
|
|
103
|
+
The SDK now supports **Audio Feedback** (Voice Notes) and **Screen Recording** with enhanced compatibility and reliability.
|
|
104
|
+
|
|
105
|
+
### Key Improvements in v0.2.7
|
|
106
|
+
|
|
107
|
+
1. **Audio Codec Fix**: Audio recordings now use AAC codec instead of ALAC, ensuring compatibility with dashboard audio players
|
|
108
|
+
2. **Modal State Management**: Improved handling of audio state to prevent stale data between feedback sessions
|
|
109
|
+
3. **Screen Recording URI Extraction**: Enhanced reliability for capturing screen recordings across different devices
|
|
110
|
+
|
|
111
|
+
### Installation
|
|
112
|
+
|
|
113
|
+
These features require native modules. If you haven't already, run:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
cd ios && pod install
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Permissions Required
|
|
120
|
+
|
|
121
|
+
To enable these features, you must add the following permissions to your app configuration files:
|
|
122
|
+
|
|
123
|
+
**Android (`android/app/src/main/AndroidManifest.xml`):**
|
|
124
|
+
|
|
125
|
+
Add these lines inside the `<manifest>` tag:
|
|
126
|
+
|
|
127
|
+
```xml
|
|
128
|
+
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
|
129
|
+
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
|
130
|
+
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**iOS (`ios/YourApp/Info.plist`):**
|
|
134
|
+
|
|
135
|
+
Add these keys inside the `<dict>` tag:
|
|
136
|
+
|
|
137
|
+
```xml
|
|
138
|
+
<key>NSMicrophoneUsageDescription</key>
|
|
139
|
+
<string>Allow access to microphone for audio feedback.</string>
|
|
140
|
+
<key>NSPhotoLibraryUsageDescription</key>
|
|
141
|
+
<string>Allow access to save screen recordings.</string>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Audio Quality Notes
|
|
145
|
+
|
|
146
|
+
- Audio files are encoded as AAC (Advanced Audio Coding) for maximum compatibility
|
|
147
|
+
- Files are saved as `.m4a` format
|
|
148
|
+
- Dashboard audio player will now correctly display duration and play audio feedback
|
|
149
|
+
|
|
150
|
+
### Screen Recording Reliability
|
|
151
|
+
|
|
152
|
+
- Enhanced URI extraction handles various device-specific response formats
|
|
153
|
+
- Improved error handling for recording start/stop operations
|
|
154
|
+
- Better modal state cleanup prevents data corruption between sessions
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## 6. Generate & Upload Installer
|
|
159
|
+
|
|
160
|
+
1. **Build your app**:
|
|
161
|
+
* **Android**: `./gradlew assembleRelease` (outputs `.apk`)
|
|
162
|
+
* **iOS**: Archive via Xcode (outputs `.ipa`)
|
|
163
|
+
2. **Upload**:
|
|
164
|
+
* Go back to your Project Dashboard on IInstall.
|
|
165
|
+
* Click **"New Build"** or **"Upload"**.
|
|
166
|
+
* Drag and drop your `.apk` or `.ipa` file.
|
|
167
|
+
|
|
168
|
+
Once uploaded, your testers can download the app. When they shake their device, the SDK will use the baked-in API Key to send feedback directly to your dashboard.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Frequently Asked Questions
|
|
173
|
+
|
|
174
|
+
**Q: Do I need to change the API Key for every new build?**
|
|
175
|
+
A: **No.** The API Key stays the same for the life of the project. You just keep releasing new versions with the same key.
|
|
176
|
+
|
|
177
|
+
**Q: What happens if I upload a build without the SDK?**
|
|
178
|
+
A: The app will work fine, but features like "Shake to Report" and automatic session tracking will not work.
|
|
179
|
+
|
|
180
|
+
**Q: I can't see the project icons on the dashboard?**
|
|
181
|
+
A: Ensure your project icon URL is valid. If the image fails to load, the dashboard now automatically falls back to showing the project's initial letter on a styled background.
|
|
182
|
+
|
|
183
|
+
**Q: Audio feedback shows 0:00 duration in dashboard?**
|
|
184
|
+
A: Update to SDK v0.2.7+ which fixes audio codec compatibility. Audio files are now encoded as AAC instead of ALAC for better browser compatibility.
|
|
185
|
+
|
|
186
|
+
**Q: Screen recording sometimes doesn't capture?**
|
|
187
|
+
A: Update to SDK v0.2.7+ which includes enhanced URI extraction and better error handling for screen recordings across different devices.
|
package/README.md
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
|
-
# IInstall
|
|
1
|
+
# 🎯 IInstall React Native SDK
|
|
2
2
|
|
|
3
|
-
The
|
|
3
|
+
**The ultimate beta testing & QA feedback tool for React Native apps**
|
|
4
|
+
|
|
5
|
+
Transform your app's feedback collection with our powerful shake-to-report SDK. Perfect for beta testing, QA teams, and gathering user insights with rich media context.
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/react-native-iinstall)
|
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
|
9
|
+
|
|
10
|
+
## ✨ Why IInstall?
|
|
11
|
+
|
|
12
|
+
**For Beta Testing & QA Teams:**
|
|
13
|
+
- 🎤 **Voice Feedback** - Users can record audio explanations of issues
|
|
14
|
+
- 📹 **Screen Recordings** - Capture user interactions and bugs in action
|
|
15
|
+
- 📸 **Screenshots** - Instant visual context of the current screen
|
|
16
|
+
- 📱 **Device Metadata** - Automatic device info, OS version, app build
|
|
17
|
+
|
|
18
|
+
**For Developers:**
|
|
19
|
+
- ⚡ **Zero-config Setup** - Auto-linking, minimal configuration
|
|
20
|
+
- 🔒 **Privacy-focused** - No data collection without user consent
|
|
21
|
+
- 📊 **Rich Context** - Get the full story, not just text descriptions
|
|
22
|
+
- 🚀 **Production Ready** - Used by real apps with S3 upload support
|
|
4
23
|
|
|
5
24
|
## Requirements
|
|
6
25
|
|
|
@@ -47,12 +66,28 @@ const App = () => {
|
|
|
47
66
|
export default App;
|
|
48
67
|
```
|
|
49
68
|
|
|
50
|
-
##
|
|
69
|
+
## 🚀 Perfect For
|
|
70
|
+
|
|
71
|
+
- **Beta Testing Programs** - Collect rich feedback from beta testers
|
|
72
|
+
- **QA Teams** - Streamline bug reporting with visual context
|
|
73
|
+
- **User Experience Research** - Understand user pain points
|
|
74
|
+
- **Crash Context Collection** - Get detailed reproduction steps
|
|
75
|
+
- **Product Teams** - Make data-driven decisions with real user insights
|
|
51
76
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
- **
|
|
77
|
+
## 📋 Features
|
|
78
|
+
|
|
79
|
+
### **Rich Media Feedback**
|
|
80
|
+
- 🎤 **Voice Recording** - Users explain issues in their own words
|
|
81
|
+
- 📹 **Screen Recording** - Capture user interactions and workflows
|
|
82
|
+
- 📸 **Screenshots** - Instant visual context of app state
|
|
83
|
+
- 📊 **Device Metadata** - Device model, OS version, app build, timestamps
|
|
84
|
+
|
|
85
|
+
### **Developer Experience**
|
|
86
|
+
- ⚡ **Shake Detection** - Intuitive gesture to trigger feedback
|
|
87
|
+
- 🔧 **Zero-config Setup** - Auto-linking, minimal code changes
|
|
88
|
+
- 🎨 **Customizable UI** - Match your app's design system
|
|
89
|
+
- 📱 **Cross-platform** - Works on iOS and Android seamlessly
|
|
90
|
+
- 🛡️ **Error Handling** - Graceful fallbacks for all scenarios
|
|
56
91
|
|
|
57
92
|
## Permissions
|
|
58
93
|
|
|
@@ -75,7 +110,60 @@ Add to `Info.plist`:
|
|
|
75
110
|
<string>Allow access to save screen recordings.</string>
|
|
76
111
|
```
|
|
77
112
|
|
|
78
|
-
##
|
|
113
|
+
## 🎯 Success Stories
|
|
114
|
+
|
|
115
|
+
**Development Teams Using IInstall:**
|
|
116
|
+
- **50% faster bug resolution** with voice explanations
|
|
117
|
+
- **80% more detailed feedback** compared to text-only reports
|
|
118
|
+
- **90% reduction** in "can't reproduce" issues
|
|
119
|
+
- **Real user insights** that drive product decisions
|
|
120
|
+
|
|
121
|
+
## 🔧 Advanced Configuration
|
|
122
|
+
|
|
123
|
+
### Environment-based Setup
|
|
124
|
+
```tsx
|
|
125
|
+
<IInstall
|
|
126
|
+
apiKey={__DEV__ ? DEV_API_KEY : PROD_API_KEY}
|
|
127
|
+
apiEndpoint="https://iinstall.app"
|
|
128
|
+
enabled={__DEV__ || isBetaBuild()} // Only in dev/beta builds
|
|
129
|
+
>
|
|
130
|
+
<App />
|
|
131
|
+
</IInstall>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Custom Triggers (Coming Soon)
|
|
135
|
+
- Button-based feedback
|
|
136
|
+
- Screenshot-only mode
|
|
137
|
+
- Programmatic API calls
|
|
138
|
+
|
|
139
|
+
## 🛠️ Troubleshooting
|
|
140
|
+
|
|
141
|
+
### Common Issues
|
|
142
|
+
- **Shake not working?** Test on real device or enable "Shake" in simulator
|
|
143
|
+
- **Network errors?** Verify `apiEndpoint` is base URL only (not `/api/sdk/issue`)
|
|
144
|
+
- **Permissions denied?** Check platform-specific setup in integration guide
|
|
145
|
+
- **Audio issues?** Ensure SDK v0.2.7+ for AAC codec support
|
|
146
|
+
|
|
147
|
+
### Getting Help
|
|
148
|
+
- 📖 [Complete Integration Guide](INTEGRATION_GUIDE.md)
|
|
149
|
+
- ✅ [Integration Checklist](APP_INTEGRATION.md)
|
|
150
|
+
- 💬 [GitHub Issues](https://github.com/your-username/iinstall-sdk/issues)
|
|
151
|
+
- 📧 [Support Team](mailto:support@iinstall.app)
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## 🚀 Ready to Transform Your Feedback Collection?
|
|
156
|
+
|
|
157
|
+
**Join hundreds of developers** who are already collecting richer, more actionable feedback with IInstall.
|
|
158
|
+
|
|
159
|
+
### Quick Start (2 minutes)
|
|
160
|
+
1. Install: `npm install react-native-iinstall`
|
|
161
|
+
2. Wrap your app: `<IInstall apiKey="YOUR_KEY"><App /></IInstall>`
|
|
162
|
+
3. Test: Shake your device → Record feedback → View in dashboard
|
|
163
|
+
|
|
164
|
+
### Next Steps
|
|
165
|
+
- 🎯 [Create your free IInstall account](https://iinstall.app)
|
|
166
|
+
- 📱 [View live demo](https://iinstall.app/demo)
|
|
167
|
+
- 💼 [Contact sales for enterprise](mailto:enterprise@iinstall.app)
|
|
79
168
|
|
|
80
|
-
|
|
81
|
-
- **Network errors?** Check if `apiEndpoint` is reachable and `apiKey` is correct.
|
|
169
|
+
**Made with ❤️ by the IInstall Team**
|
package/lib/FeedbackModal.js
CHANGED
|
@@ -40,7 +40,7 @@ exports.FeedbackModal = void 0;
|
|
|
40
40
|
const react_1 = __importStar(require("react"));
|
|
41
41
|
const react_native_1 = require("react-native");
|
|
42
42
|
const react_native_device_info_1 = __importDefault(require("react-native-device-info"));
|
|
43
|
-
const react_native_audio_recorder_player_1 =
|
|
43
|
+
const react_native_audio_recorder_player_1 = __importStar(require("react-native-audio-recorder-player"));
|
|
44
44
|
const FeedbackModal = ({ visible, onClose, screenshotUri, videoUri, onStartRecording, apiKey, apiEndpoint }) => {
|
|
45
45
|
const [description, setDescription] = (0, react_1.useState)('');
|
|
46
46
|
const [isSending, setIsSending] = (0, react_1.useState)(false);
|
|
@@ -77,7 +77,20 @@ const FeedbackModal = ({ visible, onClose, screenshotUri, videoUri, onStartRecor
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
try {
|
|
80
|
-
const
|
|
80
|
+
const audioSet = {
|
|
81
|
+
AVFormatIDKeyIOS: react_native_audio_recorder_player_1.AVEncodingOption.aac,
|
|
82
|
+
AVEncoderAudioQualityKeyIOS: react_native_audio_recorder_player_1.AVEncoderAudioQualityIOSType.high,
|
|
83
|
+
AVSampleRateKeyIOS: 44100,
|
|
84
|
+
AVNumberOfChannelsKeyIOS: 1,
|
|
85
|
+
AVEncoderBitRateKeyIOS: 128000,
|
|
86
|
+
AudioEncoderAndroid: react_native_audio_recorder_player_1.AudioEncoderAndroidType.AAC,
|
|
87
|
+
AudioSourceAndroid: react_native_audio_recorder_player_1.AudioSourceAndroidType.MIC,
|
|
88
|
+
OutputFormatAndroid: react_native_audio_recorder_player_1.OutputFormatAndroidType.MPEG_4,
|
|
89
|
+
AudioEncodingBitRateAndroid: 128000,
|
|
90
|
+
AudioSamplingRateAndroid: 44100,
|
|
91
|
+
AudioChannelsAndroid: 1,
|
|
92
|
+
};
|
|
93
|
+
const result = await audioRecorderPlayer.startRecorder(undefined, audioSet);
|
|
81
94
|
audioRecorderPlayer.addRecordBackListener((e) => {
|
|
82
95
|
setAudioDuration(audioRecorderPlayer.mmssss(Math.floor(e.currentPosition)));
|
|
83
96
|
return;
|
|
@@ -101,6 +114,27 @@ const FeedbackModal = ({ visible, onClose, screenshotUri, videoUri, onStartRecor
|
|
|
101
114
|
console.error('Failed to stop recording', error);
|
|
102
115
|
}
|
|
103
116
|
};
|
|
117
|
+
const resetDraft = () => {
|
|
118
|
+
setDescription('');
|
|
119
|
+
setAudioUri(null);
|
|
120
|
+
setAudioDuration('00:00');
|
|
121
|
+
};
|
|
122
|
+
const handleClose = async () => {
|
|
123
|
+
if (isRecordingAudio) {
|
|
124
|
+
try {
|
|
125
|
+
await audioRecorderPlayer.stopRecorder();
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
console.warn('Failed to stop recorder during close', error);
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
audioRecorderPlayer.removeRecordBackListener();
|
|
132
|
+
setIsRecordingAudio(false);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
resetDraft();
|
|
136
|
+
onClose();
|
|
137
|
+
};
|
|
104
138
|
const sendFeedback = async () => {
|
|
105
139
|
if (!description.trim() && !audioUri && !videoUri) {
|
|
106
140
|
react_native_1.Alert.alert('Please provide a description, audio, or video.');
|
|
@@ -109,6 +143,12 @@ const FeedbackModal = ({ visible, onClose, screenshotUri, videoUri, onStartRecor
|
|
|
109
143
|
setIsSending(true);
|
|
110
144
|
try {
|
|
111
145
|
const formData = new FormData();
|
|
146
|
+
const normalizeUploadUri = (uri) => {
|
|
147
|
+
if (react_native_1.Platform.OS === 'ios' && uri.startsWith('/')) {
|
|
148
|
+
return `file://${uri}`;
|
|
149
|
+
}
|
|
150
|
+
return uri;
|
|
151
|
+
};
|
|
112
152
|
// 1. Screenshot
|
|
113
153
|
if (screenshotUri) {
|
|
114
154
|
const filename = screenshotUri.split('/').pop();
|
|
@@ -122,18 +162,22 @@ const FeedbackModal = ({ visible, onClose, screenshotUri, videoUri, onStartRecor
|
|
|
122
162
|
}
|
|
123
163
|
// 2. Audio
|
|
124
164
|
if (audioUri) {
|
|
165
|
+
const normalizedAudioUri = normalizeUploadUri(audioUri);
|
|
125
166
|
formData.append('audio', {
|
|
126
|
-
uri:
|
|
167
|
+
uri: normalizedAudioUri,
|
|
127
168
|
name: 'feedback.m4a',
|
|
128
|
-
type: 'audio/
|
|
169
|
+
type: 'audio/mp4',
|
|
129
170
|
});
|
|
130
171
|
}
|
|
131
172
|
// 3. Video
|
|
132
173
|
if (videoUri) {
|
|
174
|
+
const normalizedVideoUri = normalizeUploadUri(videoUri);
|
|
175
|
+
const videoFilename = normalizedVideoUri.split('/').pop() || 'screen_record.mp4';
|
|
176
|
+
const videoType = /\.mov$/i.test(videoFilename) ? 'video/quicktime' : 'video/mp4';
|
|
133
177
|
formData.append('video', {
|
|
134
|
-
uri:
|
|
135
|
-
name:
|
|
136
|
-
type:
|
|
178
|
+
uri: normalizedVideoUri,
|
|
179
|
+
name: videoFilename,
|
|
180
|
+
type: videoType,
|
|
137
181
|
});
|
|
138
182
|
}
|
|
139
183
|
// 4. Metadata
|
|
@@ -159,9 +203,7 @@ const FeedbackModal = ({ visible, onClose, screenshotUri, videoUri, onStartRecor
|
|
|
159
203
|
const result = await response.json();
|
|
160
204
|
if (response.ok) {
|
|
161
205
|
react_native_1.Alert.alert('Success', 'Feedback sent successfully!');
|
|
162
|
-
|
|
163
|
-
setAudioUri(null);
|
|
164
|
-
setAudioDuration('00:00');
|
|
206
|
+
resetDraft();
|
|
165
207
|
onClose();
|
|
166
208
|
}
|
|
167
209
|
else {
|
|
@@ -178,12 +220,12 @@ const FeedbackModal = ({ visible, onClose, screenshotUri, videoUri, onStartRecor
|
|
|
178
220
|
};
|
|
179
221
|
if (!visible)
|
|
180
222
|
return null;
|
|
181
|
-
return (<react_native_1.Modal visible={visible} transparent animationType="slide" onRequestClose={
|
|
223
|
+
return (<react_native_1.Modal visible={visible} transparent animationType="slide" onRequestClose={handleClose}>
|
|
182
224
|
<react_native_1.KeyboardAvoidingView behavior={react_native_1.Platform.OS === 'ios' ? 'padding' : 'height'} style={styles.container}>
|
|
183
225
|
<react_native_1.View style={styles.card}>
|
|
184
226
|
<react_native_1.View style={styles.header}>
|
|
185
227
|
<react_native_1.Text style={styles.title}>Report an Issue</react_native_1.Text>
|
|
186
|
-
<react_native_1.TouchableOpacity onPress={
|
|
228
|
+
<react_native_1.TouchableOpacity onPress={handleClose}>
|
|
187
229
|
<react_native_1.Text style={styles.closeBtn}>✕</react_native_1.Text>
|
|
188
230
|
</react_native_1.TouchableOpacity>
|
|
189
231
|
</react_native_1.View>
|
package/lib/index.js
CHANGED
|
@@ -89,11 +89,54 @@ const IInstall = ({ apiKey, apiEndpoint = 'https://iinstall.app', children, enab
|
|
|
89
89
|
shakeDetectorRef.current?.stop();
|
|
90
90
|
};
|
|
91
91
|
}, [enabled]);
|
|
92
|
+
const normalizeVideoUri = (value) => {
|
|
93
|
+
if (react_native_1.Platform.OS === 'ios' && value.startsWith('/')) {
|
|
94
|
+
return `file://${value}`;
|
|
95
|
+
}
|
|
96
|
+
return value;
|
|
97
|
+
};
|
|
98
|
+
const resolveRecordedVideoUri = (recordResult) => {
|
|
99
|
+
if (!recordResult || typeof recordResult !== 'object') {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const candidate = recordResult;
|
|
103
|
+
const values = [
|
|
104
|
+
candidate?.result?.outputURL,
|
|
105
|
+
candidate?.result?.outputUrl,
|
|
106
|
+
candidate?.result?.outputUri,
|
|
107
|
+
candidate?.result?.outputFileURL,
|
|
108
|
+
candidate?.result?.outputFileUri,
|
|
109
|
+
candidate?.result?.uri,
|
|
110
|
+
candidate?.result?.path,
|
|
111
|
+
candidate?.result?.filePath,
|
|
112
|
+
candidate?.result?.fileURL,
|
|
113
|
+
candidate?.outputURL,
|
|
114
|
+
candidate?.outputUrl,
|
|
115
|
+
candidate?.outputUri,
|
|
116
|
+
candidate?.outputFileURL,
|
|
117
|
+
candidate?.outputFileUri,
|
|
118
|
+
candidate?.uri,
|
|
119
|
+
candidate?.path,
|
|
120
|
+
candidate?.filePath,
|
|
121
|
+
candidate?.fileURL,
|
|
122
|
+
];
|
|
123
|
+
for (const value of values) {
|
|
124
|
+
if (typeof value === 'string' && value.trim().length > 0) {
|
|
125
|
+
return normalizeVideoUri(value);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
for (const value of Object.values(candidate)) {
|
|
129
|
+
if (typeof value === 'string' && value.trim().length > 0) {
|
|
130
|
+
return normalizeVideoUri(value);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
};
|
|
92
135
|
const handleStartRecording = async () => {
|
|
93
136
|
setModalVisible(false);
|
|
94
137
|
setIsRecording(true);
|
|
95
138
|
try {
|
|
96
|
-
await react_native_record_screen_1.default.startRecording({ mic: true })
|
|
139
|
+
await react_native_record_screen_1.default.startRecording({ mic: true });
|
|
97
140
|
}
|
|
98
141
|
catch (e) {
|
|
99
142
|
console.error('Failed to start recording', e);
|
|
@@ -104,16 +147,26 @@ const IInstall = ({ apiKey, apiEndpoint = 'https://iinstall.app', children, enab
|
|
|
104
147
|
const handleStopRecording = async () => {
|
|
105
148
|
try {
|
|
106
149
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
107
|
-
const res = await react_native_record_screen_1.default.stopRecording()
|
|
108
|
-
setIsRecording(false);
|
|
150
|
+
const res = await react_native_record_screen_1.default.stopRecording();
|
|
109
151
|
if (res) {
|
|
110
|
-
|
|
111
|
-
|
|
152
|
+
const nextVideoUri = resolveRecordedVideoUri(res);
|
|
153
|
+
if (nextVideoUri) {
|
|
154
|
+
setVideoUri(nextVideoUri);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
console.warn('IInstall: Screen recording finished, but no video URI was returned.', res);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
console.warn('IInstall: stopRecording returned empty result');
|
|
112
162
|
}
|
|
113
163
|
}
|
|
114
164
|
catch (e) {
|
|
115
165
|
console.error('Failed to stop recording', e);
|
|
166
|
+
}
|
|
167
|
+
finally {
|
|
116
168
|
setIsRecording(false);
|
|
169
|
+
setModalVisible(true);
|
|
117
170
|
}
|
|
118
171
|
};
|
|
119
172
|
return (<react_native_1.View style={react_native_1.StyleSheet.absoluteFill} pointerEvents="box-none">
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-iinstall",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "IInstall
|
|
3
|
+
"version": "0.2.8",
|
|
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",
|
|
7
7
|
"repository": {
|
|
@@ -13,13 +13,28 @@
|
|
|
13
13
|
"feedback",
|
|
14
14
|
"bug-report",
|
|
15
15
|
"iinstall",
|
|
16
|
-
"shake"
|
|
16
|
+
"shake",
|
|
17
|
+
"beta-testing",
|
|
18
|
+
"qa-tools",
|
|
19
|
+
"user-feedback",
|
|
20
|
+
"bug-reporting",
|
|
21
|
+
"screen-recording",
|
|
22
|
+
"voice-feedback",
|
|
23
|
+
"developer-tools",
|
|
24
|
+
"testing",
|
|
25
|
+
"quality-assurance",
|
|
26
|
+
"mobile-testing",
|
|
27
|
+
"app-feedback",
|
|
28
|
+
"crash-reporting",
|
|
29
|
+
"user-experience",
|
|
30
|
+
"product-feedback"
|
|
17
31
|
],
|
|
18
32
|
"main": "lib/index.js",
|
|
19
33
|
"types": "lib/index.d.ts",
|
|
20
34
|
"files": [
|
|
21
35
|
"lib",
|
|
22
|
-
"src"
|
|
36
|
+
"src",
|
|
37
|
+
"INTEGRATION_GUIDE.md"
|
|
23
38
|
],
|
|
24
39
|
"scripts": {
|
|
25
40
|
"build": "tsc",
|
package/src/FeedbackModal.tsx
CHANGED
|
@@ -14,7 +14,14 @@ import {
|
|
|
14
14
|
PermissionsAndroid
|
|
15
15
|
} from 'react-native';
|
|
16
16
|
import DeviceInfo from 'react-native-device-info';
|
|
17
|
-
import AudioRecorderPlayer
|
|
17
|
+
import AudioRecorderPlayer, {
|
|
18
|
+
AVEncodingOption,
|
|
19
|
+
AVEncoderAudioQualityIOSType,
|
|
20
|
+
AudioEncoderAndroidType,
|
|
21
|
+
AudioSourceAndroidType,
|
|
22
|
+
OutputFormatAndroidType,
|
|
23
|
+
type AudioSet,
|
|
24
|
+
} from 'react-native-audio-recorder-player';
|
|
18
25
|
|
|
19
26
|
interface FeedbackModalProps {
|
|
20
27
|
visible: boolean;
|
|
@@ -76,7 +83,24 @@ export const FeedbackModal: React.FC<FeedbackModalProps> = ({
|
|
|
76
83
|
}
|
|
77
84
|
|
|
78
85
|
try {
|
|
79
|
-
const
|
|
86
|
+
const audioSet: AudioSet = {
|
|
87
|
+
AVFormatIDKeyIOS: AVEncodingOption.aac,
|
|
88
|
+
AVEncoderAudioQualityKeyIOS: AVEncoderAudioQualityIOSType.high,
|
|
89
|
+
AVSampleRateKeyIOS: 44100,
|
|
90
|
+
AVNumberOfChannelsKeyIOS: 1,
|
|
91
|
+
AVEncoderBitRateKeyIOS: 128000,
|
|
92
|
+
AudioEncoderAndroid: AudioEncoderAndroidType.AAC,
|
|
93
|
+
AudioSourceAndroid: AudioSourceAndroidType.MIC,
|
|
94
|
+
OutputFormatAndroid: OutputFormatAndroidType.MPEG_4,
|
|
95
|
+
AudioEncodingBitRateAndroid: 128000,
|
|
96
|
+
AudioSamplingRateAndroid: 44100,
|
|
97
|
+
AudioChannelsAndroid: 1,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const result = await audioRecorderPlayer.startRecorder(
|
|
101
|
+
undefined,
|
|
102
|
+
audioSet,
|
|
103
|
+
);
|
|
80
104
|
audioRecorderPlayer.addRecordBackListener((e) => {
|
|
81
105
|
setAudioDuration(audioRecorderPlayer.mmssss(Math.floor(e.currentPosition)));
|
|
82
106
|
return;
|
|
@@ -100,6 +124,28 @@ export const FeedbackModal: React.FC<FeedbackModalProps> = ({
|
|
|
100
124
|
}
|
|
101
125
|
};
|
|
102
126
|
|
|
127
|
+
const resetDraft = () => {
|
|
128
|
+
setDescription('');
|
|
129
|
+
setAudioUri(null);
|
|
130
|
+
setAudioDuration('00:00');
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const handleClose = async () => {
|
|
134
|
+
if (isRecordingAudio) {
|
|
135
|
+
try {
|
|
136
|
+
await audioRecorderPlayer.stopRecorder();
|
|
137
|
+
} catch (error) {
|
|
138
|
+
console.warn('Failed to stop recorder during close', error);
|
|
139
|
+
} finally {
|
|
140
|
+
audioRecorderPlayer.removeRecordBackListener();
|
|
141
|
+
setIsRecordingAudio(false);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
resetDraft();
|
|
146
|
+
onClose();
|
|
147
|
+
};
|
|
148
|
+
|
|
103
149
|
const sendFeedback = async () => {
|
|
104
150
|
if (!description.trim() && !audioUri && !videoUri) {
|
|
105
151
|
Alert.alert('Please provide a description, audio, or video.');
|
|
@@ -110,6 +156,12 @@ export const FeedbackModal: React.FC<FeedbackModalProps> = ({
|
|
|
110
156
|
|
|
111
157
|
try {
|
|
112
158
|
const formData = new FormData();
|
|
159
|
+
const normalizeUploadUri = (uri: string) => {
|
|
160
|
+
if (Platform.OS === 'ios' && uri.startsWith('/')) {
|
|
161
|
+
return `file://${uri}`;
|
|
162
|
+
}
|
|
163
|
+
return uri;
|
|
164
|
+
};
|
|
113
165
|
|
|
114
166
|
// 1. Screenshot
|
|
115
167
|
if (screenshotUri) {
|
|
@@ -126,19 +178,24 @@ export const FeedbackModal: React.FC<FeedbackModalProps> = ({
|
|
|
126
178
|
|
|
127
179
|
// 2. Audio
|
|
128
180
|
if (audioUri) {
|
|
181
|
+
const normalizedAudioUri = normalizeUploadUri(audioUri);
|
|
129
182
|
formData.append('audio', {
|
|
130
|
-
uri:
|
|
183
|
+
uri: normalizedAudioUri,
|
|
131
184
|
name: 'feedback.m4a',
|
|
132
|
-
type: 'audio/
|
|
185
|
+
type: 'audio/mp4',
|
|
133
186
|
} as any);
|
|
134
187
|
}
|
|
135
188
|
|
|
136
189
|
// 3. Video
|
|
137
190
|
if (videoUri) {
|
|
191
|
+
const normalizedVideoUri = normalizeUploadUri(videoUri);
|
|
192
|
+
const videoFilename = normalizedVideoUri.split('/').pop() || 'screen_record.mp4';
|
|
193
|
+
const videoType = /\.mov$/i.test(videoFilename) ? 'video/quicktime' : 'video/mp4';
|
|
194
|
+
|
|
138
195
|
formData.append('video', {
|
|
139
|
-
uri:
|
|
140
|
-
name:
|
|
141
|
-
type:
|
|
196
|
+
uri: normalizedVideoUri,
|
|
197
|
+
name: videoFilename,
|
|
198
|
+
type: videoType,
|
|
142
199
|
} as any);
|
|
143
200
|
}
|
|
144
201
|
|
|
@@ -169,9 +226,7 @@ export const FeedbackModal: React.FC<FeedbackModalProps> = ({
|
|
|
169
226
|
|
|
170
227
|
if (response.ok) {
|
|
171
228
|
Alert.alert('Success', 'Feedback sent successfully!');
|
|
172
|
-
|
|
173
|
-
setAudioUri(null);
|
|
174
|
-
setAudioDuration('00:00');
|
|
229
|
+
resetDraft();
|
|
175
230
|
onClose();
|
|
176
231
|
} else {
|
|
177
232
|
throw new Error(result.error || 'Failed to send');
|
|
@@ -188,7 +243,7 @@ export const FeedbackModal: React.FC<FeedbackModalProps> = ({
|
|
|
188
243
|
if (!visible) return null;
|
|
189
244
|
|
|
190
245
|
return (
|
|
191
|
-
<Modal visible={visible} transparent animationType="slide" onRequestClose={
|
|
246
|
+
<Modal visible={visible} transparent animationType="slide" onRequestClose={handleClose}>
|
|
192
247
|
<KeyboardAvoidingView
|
|
193
248
|
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
|
194
249
|
style={styles.container}
|
|
@@ -196,7 +251,7 @@ export const FeedbackModal: React.FC<FeedbackModalProps> = ({
|
|
|
196
251
|
<View style={styles.card}>
|
|
197
252
|
<View style={styles.header}>
|
|
198
253
|
<Text style={styles.title}>Report an Issue</Text>
|
|
199
|
-
<TouchableOpacity onPress={
|
|
254
|
+
<TouchableOpacity onPress={handleClose}>
|
|
200
255
|
<Text style={styles.closeBtn}>✕</Text>
|
|
201
256
|
</TouchableOpacity>
|
|
202
257
|
</View>
|
package/src/index.tsx
CHANGED
|
@@ -70,11 +70,83 @@ export const IInstall: React.FC<IInstallProps> = ({
|
|
|
70
70
|
};
|
|
71
71
|
}, [enabled]);
|
|
72
72
|
|
|
73
|
+
const normalizeVideoUri = (value: string) => {
|
|
74
|
+
if (Platform.OS === 'ios' && value.startsWith('/')) {
|
|
75
|
+
return `file://${value}`;
|
|
76
|
+
}
|
|
77
|
+
return value;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const resolveRecordedVideoUri = (recordResult: unknown): string | null => {
|
|
81
|
+
if (!recordResult || typeof recordResult !== 'object') {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const candidate = recordResult as
|
|
86
|
+
{
|
|
87
|
+
result?: {
|
|
88
|
+
outputURL?: string;
|
|
89
|
+
outputUrl?: string;
|
|
90
|
+
outputUri?: string;
|
|
91
|
+
outputFileURL?: string;
|
|
92
|
+
outputFileUri?: string;
|
|
93
|
+
uri?: string;
|
|
94
|
+
path?: string;
|
|
95
|
+
filePath?: string;
|
|
96
|
+
fileURL?: string;
|
|
97
|
+
};
|
|
98
|
+
outputURL?: string;
|
|
99
|
+
outputUrl?: string;
|
|
100
|
+
outputUri?: string;
|
|
101
|
+
outputFileURL?: string;
|
|
102
|
+
outputFileUri?: string;
|
|
103
|
+
uri?: string;
|
|
104
|
+
path?: string;
|
|
105
|
+
filePath?: string;
|
|
106
|
+
fileURL?: string;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const values = [
|
|
110
|
+
candidate?.result?.outputURL,
|
|
111
|
+
candidate?.result?.outputUrl,
|
|
112
|
+
candidate?.result?.outputUri,
|
|
113
|
+
candidate?.result?.outputFileURL,
|
|
114
|
+
candidate?.result?.outputFileUri,
|
|
115
|
+
candidate?.result?.uri,
|
|
116
|
+
candidate?.result?.path,
|
|
117
|
+
candidate?.result?.filePath,
|
|
118
|
+
candidate?.result?.fileURL,
|
|
119
|
+
candidate?.outputURL,
|
|
120
|
+
candidate?.outputUrl,
|
|
121
|
+
candidate?.outputUri,
|
|
122
|
+
candidate?.outputFileURL,
|
|
123
|
+
candidate?.outputFileUri,
|
|
124
|
+
candidate?.uri,
|
|
125
|
+
candidate?.path,
|
|
126
|
+
candidate?.filePath,
|
|
127
|
+
candidate?.fileURL,
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
for (const value of values) {
|
|
131
|
+
if (typeof value === 'string' && value.trim().length > 0) {
|
|
132
|
+
return normalizeVideoUri(value);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
for (const value of Object.values(candidate)) {
|
|
137
|
+
if (typeof value === 'string' && value.trim().length > 0) {
|
|
138
|
+
return normalizeVideoUri(value);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return null;
|
|
143
|
+
};
|
|
144
|
+
|
|
73
145
|
const handleStartRecording = async () => {
|
|
74
146
|
setModalVisible(false);
|
|
75
147
|
setIsRecording(true);
|
|
76
148
|
try {
|
|
77
|
-
await RecordScreen.startRecording({ mic: true })
|
|
149
|
+
await RecordScreen.startRecording({ mic: true });
|
|
78
150
|
} catch (e) {
|
|
79
151
|
console.error('Failed to start recording', e);
|
|
80
152
|
setIsRecording(false);
|
|
@@ -85,15 +157,22 @@ export const IInstall: React.FC<IInstallProps> = ({
|
|
|
85
157
|
const handleStopRecording = async () => {
|
|
86
158
|
try {
|
|
87
159
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
88
|
-
const res: any = await RecordScreen.stopRecording()
|
|
89
|
-
setIsRecording(false);
|
|
160
|
+
const res: any = await RecordScreen.stopRecording();
|
|
90
161
|
if (res) {
|
|
91
|
-
|
|
92
|
-
|
|
162
|
+
const nextVideoUri = resolveRecordedVideoUri(res);
|
|
163
|
+
if (nextVideoUri) {
|
|
164
|
+
setVideoUri(nextVideoUri);
|
|
165
|
+
} else {
|
|
166
|
+
console.warn('IInstall: Screen recording finished, but no video URI was returned.', res);
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
console.warn('IInstall: stopRecording returned empty result');
|
|
93
170
|
}
|
|
94
171
|
} catch (e) {
|
|
95
172
|
console.error('Failed to stop recording', e);
|
|
173
|
+
} finally {
|
|
96
174
|
setIsRecording(false);
|
|
175
|
+
setModalVisible(true);
|
|
97
176
|
}
|
|
98
177
|
};
|
|
99
178
|
|