react-native-iinstall 0.2.11 → 0.2.12

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.
@@ -5,6 +5,8 @@ interface IInstallWrapperProps {
5
5
  children?: React.ReactNode;
6
6
  enabled?: boolean;
7
7
  showDebugButton?: boolean;
8
+ showFloatingButtonOnEmulator?: boolean;
9
+ floatingButtonLabel?: string;
8
10
  }
9
11
  export declare const IInstallWrapper: React.FC<IInstallWrapperProps>;
10
- export {};
12
+ export default IInstallWrapper;
@@ -1,167 +1,18 @@
1
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
- })();
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
35
5
  Object.defineProperty(exports, "__esModule", { value: true });
36
6
  exports.IInstallWrapper = void 0;
37
- const react_1 = __importStar(require("react"));
38
- const react_native_1 = require("react-native");
39
- // Import from the local SDK files instead of the package
7
+ const react_1 = __importDefault(require("react"));
40
8
  const index_1 = require("./index");
41
- const ShakeDetector_1 = require("./ShakeDetector");
42
- // Helper to detect simulator/emulator
43
- const isSimulator = () => {
44
- // @ts-expect-error - __DEV__ is a React Native global
45
- const isDev = typeof global !== 'undefined' && global.__DEV__ === true;
46
- return (react_native_1.Platform.OS === 'ios' &&
47
- (react_native_1.Platform.isPad || react_native_1.Platform.isTV || isDev)) || (react_native_1.Platform.OS === 'android' && isDev);
48
- };
49
- // Helper to detect if native sensors are available
50
- const hasNativeSensors = () => {
51
- try {
52
- // Check if we're in a simulator/emulator environment
53
- if (isSimulator()) {
54
- return false;
55
- }
56
- // Additional check for Android emulators
57
- if (react_native_1.Platform.OS === 'android') {
58
- // Android emulators typically don't have motion sensors
59
- return false;
60
- }
61
- // iOS simulators don't have motion sensors
62
- if (react_native_1.Platform.OS === 'ios' && react_native_1.Platform.isPad) {
63
- return false;
64
- }
65
- return true;
66
- }
67
- catch (e) {
68
- return false;
69
- }
70
- };
71
- const IInstallWrapper = ({ apiKey, apiEndpoint = 'https://iinstall.app', children, enabled = true, showDebugButton = true }) => {
72
- const shakeDetectorRef = (0, react_1.useRef)(null);
73
- // Calculate sensor availability once to avoid state updates
74
- const sensorsAvailable = enabled && hasNativeSensors();
75
- const showManualButton = !sensorsAvailable && enabled;
76
- (0, react_1.useEffect)(() => {
77
- if (!enabled)
78
- return;
79
- // Only use shake detector if sensors are available
80
- if (sensorsAvailable) {
81
- shakeDetectorRef.current = new ShakeDetector_1.ShakeDetector(() => {
82
- // This will trigger the IInstall modal
83
- // The shake detector will handle the screenshot capture
84
- });
85
- shakeDetectorRef.current.start();
86
- }
87
- return () => {
88
- shakeDetectorRef.current?.stop();
89
- };
90
- }, [enabled, sensorsAvailable]);
91
- const handleManualReport = () => {
92
- // Manually trigger the IInstall feedback modal
93
- // This simulates a shake gesture
94
- if (enabled) {
95
- // The IInstall component will handle the screenshot and modal
96
- // We can trigger it by simulating the shake event
97
- console.log('[IInstall] Manual report triggered');
98
- }
99
- };
100
- const handleDebugInfo = () => {
101
- const debugMessage = [
102
- `Platform: ${react_native_1.Platform.OS}`,
103
- `Is Simulator: ${isSimulator() ? 'Yes' : 'No'}`,
104
- `Native Sensors: ${sensorsAvailable ? 'Available' : 'Not Available'}`,
105
- `Manual Button: ${showManualButton ? 'Visible' : 'Hidden'}`,
106
- `Shake Detection: ${sensorsAvailable ? 'Enabled' : 'Disabled'}`,
107
- ].join('\n');
108
- react_native_1.Alert.alert('IInstall Debug Info', debugMessage, [{ text: 'OK' }]);
109
- };
110
- return (<index_1.IInstall apiKey={apiKey} apiEndpoint={apiEndpoint} enabled={enabled}>
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', }) => {
13
+ return (<index_1.IInstall apiKey={apiKey} apiEndpoint={apiEndpoint} enabled={enabled} showFloatingButtonOnEmulator={showFloatingButtonOnEmulator} floatingButtonLabel={floatingButtonLabel}>
111
14
  {children}
112
-
113
- {showManualButton && (<react_native_1.View style={styles.manualButtonContainer}>
114
- <react_native_1.TouchableOpacity style={styles.manualButton} onPress={handleManualReport}>
115
- <react_native_1.Text style={styles.manualButtonText}>🐛 Report Issue</react_native_1.Text>
116
- </react_native_1.TouchableOpacity>
117
-
118
- {showDebugButton && (<react_native_1.TouchableOpacity style={styles.debugButton} onPress={handleDebugInfo}>
119
- <react_native_1.Text style={styles.debugButtonText}>ℹ️</react_native_1.Text>
120
- </react_native_1.TouchableOpacity>)}
121
- </react_native_1.View>)}
122
15
  </index_1.IInstall>);
123
16
  };
124
17
  exports.IInstallWrapper = IInstallWrapper;
125
- const styles = react_native_1.StyleSheet.create({
126
- manualButtonContainer: {
127
- position: 'absolute',
128
- bottom: 40,
129
- right: 20,
130
- flexDirection: 'row',
131
- alignItems: 'center',
132
- },
133
- manualButton: {
134
- backgroundColor: '#FF6B6B',
135
- paddingHorizontal: 16,
136
- paddingVertical: 12,
137
- borderRadius: 25,
138
- elevation: 5,
139
- shadowColor: '#000',
140
- shadowOffset: { width: 0, height: 2 },
141
- shadowOpacity: 0.2,
142
- shadowRadius: 4,
143
- marginRight: 10,
144
- },
145
- manualButtonText: {
146
- color: 'white',
147
- fontSize: 14,
148
- fontWeight: '600',
149
- },
150
- debugButton: {
151
- backgroundColor: '#4ECDC4',
152
- width: 40,
153
- height: 40,
154
- borderRadius: 20,
155
- justifyContent: 'center',
156
- alignItems: 'center',
157
- elevation: 5,
158
- shadowColor: '#000',
159
- shadowOffset: { width: 0, height: 2 },
160
- shadowOpacity: 0.2,
161
- shadowRadius: 4,
162
- },
163
- debugButtonText: {
164
- color: 'white',
165
- fontSize: 16,
166
- },
167
- });
18
+ exports.default = exports.IInstallWrapper;
package/lib/index.d.ts CHANGED
@@ -4,6 +4,8 @@ interface IInstallProps {
4
4
  apiEndpoint?: string;
5
5
  children?: React.ReactNode;
6
6
  enabled?: boolean;
7
+ showFloatingButtonOnEmulator?: boolean;
8
+ floatingButtonLabel?: string;
7
9
  }
8
10
  export declare const IInstall: React.FC<IInstallProps>;
9
11
  export default IInstall;
package/lib/index.js CHANGED
@@ -43,11 +43,13 @@ const react_native_view_shot_1 = require("react-native-view-shot");
43
43
  const ShakeDetector_1 = require("./ShakeDetector");
44
44
  const FeedbackModal_1 = require("./FeedbackModal");
45
45
  const react_native_record_screen_1 = __importDefault(require("react-native-record-screen"));
46
- const IInstall = ({ apiKey, apiEndpoint = 'https://iinstall.app', children, enabled = true }) => {
46
+ const react_native_device_info_1 = __importDefault(require("react-native-device-info"));
47
+ const IInstall = ({ apiKey, apiEndpoint = 'https://iinstall.app', children, enabled = true, showFloatingButtonOnEmulator = true, floatingButtonLabel = 'Report Issue', }) => {
47
48
  const [modalVisible, setModalVisible] = (0, react_1.useState)(false);
48
49
  const [screenshotUri, setScreenshotUri] = (0, react_1.useState)(null);
49
50
  const [videoUri, setVideoUri] = (0, react_1.useState)(null);
50
51
  const [isRecording, setIsRecording] = (0, react_1.useState)(false);
52
+ const [isEmulator, setIsEmulator] = (0, react_1.useState)(false);
51
53
  const shakeDetectorRef = (0, react_1.useRef)(null);
52
54
  // Refs for stable access in shake callback
53
55
  const isRecordingRef = (0, react_1.useRef)(isRecording);
@@ -56,6 +58,23 @@ const IInstall = ({ apiKey, apiEndpoint = 'https://iinstall.app', children, enab
56
58
  isRecordingRef.current = isRecording;
57
59
  modalVisibleRef.current = modalVisible;
58
60
  }, [isRecording, modalVisible]);
61
+ (0, react_1.useEffect)(() => {
62
+ let mounted = true;
63
+ react_native_device_info_1.default.isEmulator()
64
+ .then((value) => {
65
+ if (mounted) {
66
+ setIsEmulator(value);
67
+ }
68
+ })
69
+ .catch(() => {
70
+ if (mounted) {
71
+ setIsEmulator(false);
72
+ }
73
+ });
74
+ return () => {
75
+ mounted = false;
76
+ };
77
+ }, []);
59
78
  const handleShake = async () => {
60
79
  if (modalVisibleRef.current || isRecordingRef.current)
61
80
  return;
@@ -80,7 +99,7 @@ const IInstall = ({ apiKey, apiEndpoint = 'https://iinstall.app', children, enab
80
99
  handleShakeCallback.current = handleShake;
81
100
  });
82
101
  (0, react_1.useEffect)(() => {
83
- if (!enabled)
102
+ if (!enabled || isEmulator)
84
103
  return;
85
104
  // Use a wrapper to call the current handleShake
86
105
  shakeDetectorRef.current = new ShakeDetector_1.ShakeDetector(() => handleShakeCallback.current());
@@ -88,7 +107,15 @@ const IInstall = ({ apiKey, apiEndpoint = 'https://iinstall.app', children, enab
88
107
  return () => {
89
108
  shakeDetectorRef.current?.stop();
90
109
  };
91
- }, [enabled]);
110
+ }, [enabled, isEmulator]);
111
+ const handleFloatingButtonPress = async () => {
112
+ await handleShakeCallback.current();
113
+ };
114
+ const shouldShowFloatingButton = enabled &&
115
+ isEmulator &&
116
+ showFloatingButtonOnEmulator &&
117
+ !modalVisible &&
118
+ !isRecording;
92
119
  const normalizeVideoUri = (value) => {
93
120
  if (react_native_1.Platform.OS === 'ios' && value.startsWith('/')) {
94
121
  return `file://${value}`;
@@ -173,6 +200,12 @@ const IInstall = ({ apiKey, apiEndpoint = 'https://iinstall.app', children, enab
173
200
  <react_native_1.View style={react_native_1.StyleSheet.absoluteFill}>
174
201
  {children}
175
202
  </react_native_1.View>
203
+
204
+ {shouldShowFloatingButton && (<react_native_1.View style={styles.floatingButtonContainer} pointerEvents="box-none">
205
+ <react_native_1.TouchableOpacity accessibilityRole="button" accessibilityLabel="Open feedback menu" onPress={handleFloatingButtonPress} style={styles.floatingButton}>
206
+ <react_native_1.Text style={styles.floatingButtonText}>{floatingButtonLabel}</react_native_1.Text>
207
+ </react_native_1.TouchableOpacity>
208
+ </react_native_1.View>)}
176
209
 
177
210
  {isRecording && (<react_native_1.SafeAreaView style={styles.recordingOverlay} pointerEvents="box-none">
178
211
  <react_native_1.View style={styles.recordingContainer}>
@@ -193,6 +226,30 @@ const IInstall = ({ apiKey, apiEndpoint = 'https://iinstall.app', children, enab
193
226
  };
194
227
  exports.IInstall = IInstall;
195
228
  const styles = react_native_1.StyleSheet.create({
229
+ floatingButtonContainer: {
230
+ position: 'absolute',
231
+ right: 16,
232
+ bottom: 28,
233
+ zIndex: 9998,
234
+ elevation: 20,
235
+ },
236
+ floatingButton: {
237
+ backgroundColor: '#0f172a',
238
+ borderColor: '#2563eb',
239
+ borderWidth: 1,
240
+ borderRadius: 24,
241
+ paddingHorizontal: 14,
242
+ paddingVertical: 10,
243
+ shadowColor: '#000',
244
+ shadowOffset: { width: 0, height: 2 },
245
+ shadowOpacity: 0.35,
246
+ shadowRadius: 4,
247
+ },
248
+ floatingButtonText: {
249
+ color: '#FFF',
250
+ fontWeight: '700',
251
+ fontSize: 13,
252
+ },
196
253
  recordingOverlay: {
197
254
  position: 'absolute',
198
255
  top: 50,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-iinstall",
3
- "version": "0.2.11",
3
+ "version": "0.2.12",
4
4
  "description": "🎯 IInstall React Native SDK - The ultimate beta testing & QA feedback tool. Shake-to-report with voice recordings, screen recordings, and screenshots. Zero-config setup with TypeScript support. Perfect for beta testing, QA teams, and user feedback collection.",
5
5
  "author": "TesterFlow Team",
6
6
  "license": "MIT",
@@ -1,178 +1,40 @@
1
- import React, { useEffect, useState, useRef } from 'react';
2
- import { View, StyleSheet, TouchableOpacity, Text, Platform, Alert } from 'react-native';
3
- // Import from the local SDK files instead of the package
1
+ import React from 'react';
4
2
  import { IInstall } from './index';
5
- import { ShakeDetector } from './ShakeDetector';
6
3
 
7
4
  interface IInstallWrapperProps {
8
5
  apiKey: string;
9
6
  apiEndpoint?: string;
10
7
  children?: React.ReactNode;
11
8
  enabled?: boolean;
9
+ // Backward compatibility only. No longer used.
12
10
  showDebugButton?: boolean;
11
+ showFloatingButtonOnEmulator?: boolean;
12
+ floatingButtonLabel?: string;
13
13
  }
14
14
 
15
- // Helper to detect simulator/emulator
16
- const isSimulator = () => {
17
- // @ts-expect-error - __DEV__ is a React Native global
18
- const isDev = typeof global !== 'undefined' && global.__DEV__ === true;
19
-
20
- return (
21
- Platform.OS === 'ios' &&
22
- (Platform.isPad || Platform.isTV || isDev)
23
- ) || (
24
- Platform.OS === 'android' && isDev
25
- );
26
- };
27
-
28
- // Helper to detect if native sensors are available
29
- const hasNativeSensors = () => {
30
- try {
31
- // Check if we're in a simulator/emulator environment
32
- if (isSimulator()) {
33
- return false;
34
- }
35
-
36
- // Additional check for Android emulators
37
- if (Platform.OS === 'android') {
38
- // Android emulators typically don't have motion sensors
39
- return false;
40
- }
41
-
42
- // iOS simulators don't have motion sensors
43
- if (Platform.OS === 'ios' && Platform.isPad) {
44
- return false;
45
- }
46
-
47
- return true;
48
- } catch (e) {
49
- return false;
50
- }
51
- };
52
-
53
- export const IInstallWrapper: React.FC<IInstallWrapperProps> = ({
54
- apiKey,
55
- apiEndpoint = 'https://iinstall.app',
15
+ // Backward-compatible wrapper. The core IInstall component now handles:
16
+ // - shake gesture on real devices
17
+ // - floating manual trigger button on simulator/emulator
18
+ export const IInstallWrapper: React.FC<IInstallWrapperProps> = ({
19
+ apiKey,
20
+ apiEndpoint = 'https://iinstall.app',
56
21
  children,
57
22
  enabled = true,
58
- showDebugButton = true
23
+ showDebugButton: _showDebugButton = false,
24
+ showFloatingButtonOnEmulator = true,
25
+ floatingButtonLabel = 'Report Issue',
59
26
  }) => {
60
- const shakeDetectorRef = useRef<ShakeDetector | null>(null);
61
-
62
- // Calculate sensor availability once to avoid state updates
63
- const sensorsAvailable = enabled && hasNativeSensors();
64
- const showManualButton = !sensorsAvailable && enabled;
65
-
66
- useEffect(() => {
67
- if (!enabled) return;
68
-
69
- // Only use shake detector if sensors are available
70
- if (sensorsAvailable) {
71
- shakeDetectorRef.current = new ShakeDetector(() => {
72
- // This will trigger the IInstall modal
73
- // The shake detector will handle the screenshot capture
74
- });
75
- shakeDetectorRef.current.start();
76
- }
77
-
78
- return () => {
79
- shakeDetectorRef.current?.stop();
80
- };
81
- }, [enabled, sensorsAvailable]);
82
-
83
- const handleManualReport = () => {
84
- // Manually trigger the IInstall feedback modal
85
- // This simulates a shake gesture
86
- if (enabled) {
87
- // The IInstall component will handle the screenshot and modal
88
- // We can trigger it by simulating the shake event
89
- console.log('[IInstall] Manual report triggered');
90
- }
91
- };
92
-
93
- const handleDebugInfo = () => {
94
- const debugMessage = [
95
- `Platform: ${Platform.OS}`,
96
- `Is Simulator: ${isSimulator() ? 'Yes' : 'No'}`,
97
- `Native Sensors: ${sensorsAvailable ? 'Available' : 'Not Available'}`,
98
- `Manual Button: ${showManualButton ? 'Visible' : 'Hidden'}`,
99
- `Shake Detection: ${sensorsAvailable ? 'Enabled' : 'Disabled'}`,
100
- ].join('\n');
101
-
102
- Alert.alert('IInstall Debug Info', debugMessage, [{ text: 'OK' }]);
103
- };
104
-
105
27
  return (
106
- <IInstall
107
- apiKey={apiKey}
108
- apiEndpoint={apiEndpoint}
28
+ <IInstall
29
+ apiKey={apiKey}
30
+ apiEndpoint={apiEndpoint}
109
31
  enabled={enabled}
32
+ showFloatingButtonOnEmulator={showFloatingButtonOnEmulator}
33
+ floatingButtonLabel={floatingButtonLabel}
110
34
  >
111
35
  {children}
112
-
113
- {showManualButton && (
114
- <View style={styles.manualButtonContainer}>
115
- <TouchableOpacity
116
- style={styles.manualButton}
117
- onPress={handleManualReport}
118
- >
119
- <Text style={styles.manualButtonText}>🐛 Report Issue</Text>
120
- </TouchableOpacity>
121
-
122
- {showDebugButton && (
123
- <TouchableOpacity
124
- style={styles.debugButton}
125
- onPress={handleDebugInfo}
126
- >
127
- <Text style={styles.debugButtonText}>ℹ️</Text>
128
- </TouchableOpacity>
129
- )}
130
- </View>
131
- )}
132
36
  </IInstall>
133
37
  );
134
38
  };
135
39
 
136
- const styles = StyleSheet.create({
137
- manualButtonContainer: {
138
- position: 'absolute',
139
- bottom: 40,
140
- right: 20,
141
- flexDirection: 'row',
142
- alignItems: 'center',
143
- },
144
- manualButton: {
145
- backgroundColor: '#FF6B6B',
146
- paddingHorizontal: 16,
147
- paddingVertical: 12,
148
- borderRadius: 25,
149
- elevation: 5,
150
- shadowColor: '#000',
151
- shadowOffset: { width: 0, height: 2 },
152
- shadowOpacity: 0.2,
153
- shadowRadius: 4,
154
- marginRight: 10,
155
- },
156
- manualButtonText: {
157
- color: 'white',
158
- fontSize: 14,
159
- fontWeight: '600',
160
- },
161
- debugButton: {
162
- backgroundColor: '#4ECDC4',
163
- width: 40,
164
- height: 40,
165
- borderRadius: 20,
166
- justifyContent: 'center',
167
- alignItems: 'center',
168
- elevation: 5,
169
- shadowColor: '#000',
170
- shadowOffset: { width: 0, height: 2 },
171
- shadowOpacity: 0.2,
172
- shadowRadius: 4,
173
- },
174
- debugButtonText: {
175
- color: 'white',
176
- fontSize: 16,
177
- },
178
- });
40
+ export default IInstallWrapper;
package/src/index.tsx CHANGED
@@ -4,24 +4,30 @@ import { captureScreen } from 'react-native-view-shot';
4
4
  import { ShakeDetector } from './ShakeDetector';
5
5
  import { FeedbackModal } from './FeedbackModal';
6
6
  import RecordScreen from 'react-native-record-screen';
7
+ import DeviceInfo from 'react-native-device-info';
7
8
 
8
9
  interface IInstallProps {
9
10
  apiKey: string;
10
11
  apiEndpoint?: string; // e.g. https://iinstall.app
11
12
  children?: React.ReactNode;
12
13
  enabled?: boolean;
14
+ showFloatingButtonOnEmulator?: boolean;
15
+ floatingButtonLabel?: string;
13
16
  }
14
17
 
15
18
  export const IInstall: React.FC<IInstallProps> = ({
16
19
  apiKey,
17
20
  apiEndpoint = 'https://iinstall.app',
18
21
  children,
19
- enabled = true
22
+ enabled = true,
23
+ showFloatingButtonOnEmulator = true,
24
+ floatingButtonLabel = 'Report Issue',
20
25
  }) => {
21
26
  const [modalVisible, setModalVisible] = useState(false);
22
27
  const [screenshotUri, setScreenshotUri] = useState<string | null>(null);
23
28
  const [videoUri, setVideoUri] = useState<string | null>(null);
24
29
  const [isRecording, setIsRecording] = useState(false);
30
+ const [isEmulator, setIsEmulator] = useState(false);
25
31
 
26
32
  const shakeDetectorRef = useRef<ShakeDetector | null>(null);
27
33
 
@@ -34,6 +40,26 @@ export const IInstall: React.FC<IInstallProps> = ({
34
40
  modalVisibleRef.current = modalVisible;
35
41
  }, [isRecording, modalVisible]);
36
42
 
43
+ useEffect(() => {
44
+ let mounted = true;
45
+
46
+ DeviceInfo.isEmulator()
47
+ .then((value) => {
48
+ if (mounted) {
49
+ setIsEmulator(value);
50
+ }
51
+ })
52
+ .catch(() => {
53
+ if (mounted) {
54
+ setIsEmulator(false);
55
+ }
56
+ });
57
+
58
+ return () => {
59
+ mounted = false;
60
+ };
61
+ }, []);
62
+
37
63
  const handleShake = async () => {
38
64
  if (modalVisibleRef.current || isRecordingRef.current) return;
39
65
 
@@ -59,7 +85,7 @@ export const IInstall: React.FC<IInstallProps> = ({
59
85
  });
60
86
 
61
87
  useEffect(() => {
62
- if (!enabled) return;
88
+ if (!enabled || isEmulator) return;
63
89
 
64
90
  // Use a wrapper to call the current handleShake
65
91
  shakeDetectorRef.current = new ShakeDetector(() => handleShakeCallback.current());
@@ -68,7 +94,18 @@ export const IInstall: React.FC<IInstallProps> = ({
68
94
  return () => {
69
95
  shakeDetectorRef.current?.stop();
70
96
  };
71
- }, [enabled]);
97
+ }, [enabled, isEmulator]);
98
+
99
+ const handleFloatingButtonPress = async () => {
100
+ await handleShakeCallback.current();
101
+ };
102
+
103
+ const shouldShowFloatingButton =
104
+ enabled &&
105
+ isEmulator &&
106
+ showFloatingButtonOnEmulator &&
107
+ !modalVisible &&
108
+ !isRecording;
72
109
 
73
110
  const normalizeVideoUri = (value: string) => {
74
111
  if (Platform.OS === 'ios' && value.startsWith('/')) {
@@ -181,6 +218,19 @@ export const IInstall: React.FC<IInstallProps> = ({
181
218
  <View style={StyleSheet.absoluteFill}>
182
219
  {children}
183
220
  </View>
221
+
222
+ {shouldShowFloatingButton && (
223
+ <View style={styles.floatingButtonContainer} pointerEvents="box-none">
224
+ <TouchableOpacity
225
+ accessibilityRole="button"
226
+ accessibilityLabel="Open feedback menu"
227
+ onPress={handleFloatingButtonPress}
228
+ style={styles.floatingButton}
229
+ >
230
+ <Text style={styles.floatingButtonText}>{floatingButtonLabel}</Text>
231
+ </TouchableOpacity>
232
+ </View>
233
+ )}
184
234
 
185
235
  {isRecording && (
186
236
  <SafeAreaView style={styles.recordingOverlay} pointerEvents="box-none">
@@ -212,6 +262,30 @@ export const IInstall: React.FC<IInstallProps> = ({
212
262
  };
213
263
 
214
264
  const styles = StyleSheet.create({
265
+ floatingButtonContainer: {
266
+ position: 'absolute',
267
+ right: 16,
268
+ bottom: 28,
269
+ zIndex: 9998,
270
+ elevation: 20,
271
+ },
272
+ floatingButton: {
273
+ backgroundColor: '#0f172a',
274
+ borderColor: '#2563eb',
275
+ borderWidth: 1,
276
+ borderRadius: 24,
277
+ paddingHorizontal: 14,
278
+ paddingVertical: 10,
279
+ shadowColor: '#000',
280
+ shadowOffset: { width: 0, height: 2 },
281
+ shadowOpacity: 0.35,
282
+ shadowRadius: 4,
283
+ },
284
+ floatingButtonText: {
285
+ color: '#FFF',
286
+ fontWeight: '700',
287
+ fontSize: 13,
288
+ },
215
289
  recordingOverlay: {
216
290
  position: 'absolute',
217
291
  top: 50,