react-native-iinstall 0.2.11 → 0.2.13
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 +43 -5
- package/README.md +31 -0
- package/lib/IInstallWrapper.d.ts +7 -1
- package/lib/IInstallWrapper.js +10 -159
- package/lib/index.d.ts +7 -0
- package/lib/index.js +115 -4
- package/lib/pushRegistration.d.ts +22 -0
- package/lib/pushRegistration.js +70 -0
- package/package.json +1 -1
- package/src/IInstallWrapper.tsx +31 -157
- package/src/index.tsx +147 -3
- package/src/pushRegistration.ts +111 -0
package/INTEGRATION_GUIDE.md
CHANGED
|
@@ -48,14 +48,14 @@ yarn add react-native-iinstall
|
|
|
48
48
|
|
|
49
49
|
**Note**: The SDK automatically includes necessary dependencies like `react-native-sensors` and `react-native-view-shot`.
|
|
50
50
|
|
|
51
|
-
### Updating to Latest Version (v0.2.
|
|
51
|
+
### Updating to Latest Version (v0.2.13)
|
|
52
52
|
|
|
53
53
|
If you're using an older version, update to get the latest audio/video improvements:
|
|
54
54
|
|
|
55
55
|
```bash
|
|
56
|
-
npm install react-native-iinstall@0.2.
|
|
56
|
+
npm install react-native-iinstall@0.2.13
|
|
57
57
|
# OR
|
|
58
|
-
yarn add react-native-iinstall@0.2.
|
|
58
|
+
yarn add react-native-iinstall@0.2.13
|
|
59
59
|
```
|
|
60
60
|
|
|
61
61
|
**Key improvements in v0.2.7:**
|
|
@@ -98,7 +98,45 @@ export default App;
|
|
|
98
98
|
|
|
99
99
|
---
|
|
100
100
|
|
|
101
|
-
## 5.
|
|
101
|
+
## 5. Push Notification Token Registration (v0.2.13+)
|
|
102
|
+
|
|
103
|
+
If you pass `pushToken`, SDK auto-registers it to the backend using:
|
|
104
|
+
|
|
105
|
+
`POST /api/notifications/push/register`
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
import React from 'react';
|
|
109
|
+
import messaging from '@react-native-firebase/messaging';
|
|
110
|
+
import { IInstall } from 'react-native-iinstall';
|
|
111
|
+
|
|
112
|
+
const App = () => {
|
|
113
|
+
const [pushToken, setPushToken] = React.useState<string>();
|
|
114
|
+
|
|
115
|
+
React.useEffect(() => {
|
|
116
|
+
messaging().getToken().then(setPushToken).catch(() => undefined);
|
|
117
|
+
}, []);
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<IInstall
|
|
121
|
+
apiKey="YOUR_COPIED_API_KEY_FROM_STEP_1"
|
|
122
|
+
apiEndpoint="https://iinstall.app"
|
|
123
|
+
pushToken={pushToken}
|
|
124
|
+
>
|
|
125
|
+
<AppNavigation />
|
|
126
|
+
</IInstall>
|
|
127
|
+
);
|
|
128
|
+
};
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Manual helpers are also exported:
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
134
|
+
import { registerPushToken, unregisterPushToken } from 'react-native-iinstall';
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## 6. Audio & Video Feedback (v0.2.7+)
|
|
102
140
|
|
|
103
141
|
The SDK now supports **Audio Feedback** (Voice Notes) and **Screen Recording** with enhanced compatibility and reliability.
|
|
104
142
|
|
|
@@ -155,7 +193,7 @@ Add these keys inside the `<dict>` tag:
|
|
|
155
193
|
|
|
156
194
|
---
|
|
157
195
|
|
|
158
|
-
##
|
|
196
|
+
## 7. Generate & Upload Installer
|
|
159
197
|
|
|
160
198
|
1. **Build your app**:
|
|
161
199
|
* **Android**: `./gradlew assembleRelease` (outputs `.apk`)
|
package/README.md
CHANGED
|
@@ -49,13 +49,24 @@ Wrap your main app component with the `<IInstall>` provider.
|
|
|
49
49
|
import React from 'react';
|
|
50
50
|
import { IInstall } from 'react-native-iinstall';
|
|
51
51
|
import AppNavigation from './src/AppNavigation';
|
|
52
|
+
import messaging from '@react-native-firebase/messaging';
|
|
52
53
|
|
|
53
54
|
const App = () => {
|
|
55
|
+
const [pushToken, setPushToken] = React.useState<string>();
|
|
56
|
+
|
|
57
|
+
React.useEffect(() => {
|
|
58
|
+
messaging()
|
|
59
|
+
.getToken()
|
|
60
|
+
.then(setPushToken)
|
|
61
|
+
.catch(() => undefined);
|
|
62
|
+
}, []);
|
|
63
|
+
|
|
54
64
|
return (
|
|
55
65
|
// Get your API Key from the IInstall Dashboard (Project Settings)
|
|
56
66
|
<IInstall
|
|
57
67
|
apiKey="YOUR_PROJECT_API_KEY"
|
|
58
68
|
apiEndpoint="https://iinstall.app" // Optional, defaults to production
|
|
69
|
+
pushToken={pushToken} // Optional: auto-registers token to iinstall backend
|
|
59
70
|
enabled={__DEV__} // Optional: Only enable in dev/test builds
|
|
60
71
|
>
|
|
61
72
|
<AppNavigation />
|
|
@@ -131,6 +142,26 @@ Add to `Info.plist`:
|
|
|
131
142
|
</IInstall>
|
|
132
143
|
```
|
|
133
144
|
|
|
145
|
+
### Push Token Registration Helpers
|
|
146
|
+
|
|
147
|
+
You can also register/unregister tokens manually:
|
|
148
|
+
|
|
149
|
+
```tsx
|
|
150
|
+
import { registerPushToken, unregisterPushToken } from 'react-native-iinstall';
|
|
151
|
+
|
|
152
|
+
await registerPushToken({
|
|
153
|
+
token: fcmToken,
|
|
154
|
+
apiKey: 'YOUR_PROJECT_API_KEY',
|
|
155
|
+
apiEndpoint: 'https://iinstall.app',
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
await unregisterPushToken({
|
|
159
|
+
token: fcmToken,
|
|
160
|
+
apiKey: 'YOUR_PROJECT_API_KEY',
|
|
161
|
+
apiEndpoint: 'https://iinstall.app',
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
134
165
|
### Custom Triggers (Coming Soon)
|
|
135
166
|
- Button-based feedback
|
|
136
167
|
- Screenshot-only mode
|
package/lib/IInstallWrapper.d.ts
CHANGED
|
@@ -5,6 +5,12 @@ interface IInstallWrapperProps {
|
|
|
5
5
|
children?: React.ReactNode;
|
|
6
6
|
enabled?: boolean;
|
|
7
7
|
showDebugButton?: boolean;
|
|
8
|
+
showFloatingButtonOnEmulator?: boolean;
|
|
9
|
+
floatingButtonLabel?: string;
|
|
10
|
+
pushToken?: string;
|
|
11
|
+
autoRegisterPushToken?: boolean;
|
|
12
|
+
projectId?: string;
|
|
13
|
+
onPushTokenRegisterError?: (error: string) => void;
|
|
8
14
|
}
|
|
9
15
|
export declare const IInstallWrapper: React.FC<IInstallWrapperProps>;
|
|
10
|
-
export
|
|
16
|
+
export default IInstallWrapper;
|
package/lib/IInstallWrapper.js
CHANGED
|
@@ -1,167 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var
|
|
3
|
-
|
|
4
|
-
|
|
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 =
|
|
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
|
-
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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', 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}>
|
|
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
|
-
|
|
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,7 +4,14 @@ interface IInstallProps {
|
|
|
4
4
|
apiEndpoint?: string;
|
|
5
5
|
children?: React.ReactNode;
|
|
6
6
|
enabled?: boolean;
|
|
7
|
+
showFloatingButtonOnEmulator?: boolean;
|
|
8
|
+
floatingButtonLabel?: string;
|
|
9
|
+
pushToken?: string;
|
|
10
|
+
autoRegisterPushToken?: boolean;
|
|
11
|
+
projectId?: string;
|
|
12
|
+
onPushTokenRegisterError?: (error: string) => void;
|
|
7
13
|
}
|
|
8
14
|
export declare const IInstall: React.FC<IInstallProps>;
|
|
9
15
|
export default IInstall;
|
|
10
16
|
export { IInstallWrapper } from './IInstallWrapper';
|
|
17
|
+
export { registerPushToken, unregisterPushToken, type RegisterPushTokenParams, type UnregisterPushTokenParams, type PushRegistrationResult, type PushPlatform, } from './pushRegistration';
|
package/lib/index.js
CHANGED
|
@@ -36,19 +36,23 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.IInstallWrapper = exports.IInstall = void 0;
|
|
39
|
+
exports.unregisterPushToken = exports.registerPushToken = exports.IInstallWrapper = exports.IInstall = void 0;
|
|
40
40
|
const react_1 = __importStar(require("react"));
|
|
41
41
|
const react_native_1 = require("react-native");
|
|
42
42
|
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
|
|
46
|
+
const react_native_device_info_1 = __importDefault(require("react-native-device-info"));
|
|
47
|
+
const pushRegistration_1 = require("./pushRegistration");
|
|
48
|
+
const IInstall = ({ apiKey, apiEndpoint = 'https://iinstall.app', children, enabled = true, showFloatingButtonOnEmulator = true, floatingButtonLabel = 'Report Issue', pushToken, autoRegisterPushToken = true, projectId, onPushTokenRegisterError, }) => {
|
|
47
49
|
const [modalVisible, setModalVisible] = (0, react_1.useState)(false);
|
|
48
50
|
const [screenshotUri, setScreenshotUri] = (0, react_1.useState)(null);
|
|
49
51
|
const [videoUri, setVideoUri] = (0, react_1.useState)(null);
|
|
50
52
|
const [isRecording, setIsRecording] = (0, react_1.useState)(false);
|
|
53
|
+
const [isEmulator, setIsEmulator] = (0, react_1.useState)(false);
|
|
51
54
|
const shakeDetectorRef = (0, react_1.useRef)(null);
|
|
55
|
+
const lastRegisteredPushTokenRef = (0, react_1.useRef)(null);
|
|
52
56
|
// Refs for stable access in shake callback
|
|
53
57
|
const isRecordingRef = (0, react_1.useRef)(isRecording);
|
|
54
58
|
const modalVisibleRef = (0, react_1.useRef)(modalVisible);
|
|
@@ -56,6 +60,23 @@ const IInstall = ({ apiKey, apiEndpoint = 'https://iinstall.app', children, enab
|
|
|
56
60
|
isRecordingRef.current = isRecording;
|
|
57
61
|
modalVisibleRef.current = modalVisible;
|
|
58
62
|
}, [isRecording, modalVisible]);
|
|
63
|
+
(0, react_1.useEffect)(() => {
|
|
64
|
+
let mounted = true;
|
|
65
|
+
react_native_device_info_1.default.isEmulator()
|
|
66
|
+
.then((value) => {
|
|
67
|
+
if (mounted) {
|
|
68
|
+
setIsEmulator(value);
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
.catch(() => {
|
|
72
|
+
if (mounted) {
|
|
73
|
+
setIsEmulator(false);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
return () => {
|
|
77
|
+
mounted = false;
|
|
78
|
+
};
|
|
79
|
+
}, []);
|
|
59
80
|
const handleShake = async () => {
|
|
60
81
|
if (modalVisibleRef.current || isRecordingRef.current)
|
|
61
82
|
return;
|
|
@@ -80,7 +101,7 @@ const IInstall = ({ apiKey, apiEndpoint = 'https://iinstall.app', children, enab
|
|
|
80
101
|
handleShakeCallback.current = handleShake;
|
|
81
102
|
});
|
|
82
103
|
(0, react_1.useEffect)(() => {
|
|
83
|
-
if (!enabled)
|
|
104
|
+
if (!enabled || isEmulator)
|
|
84
105
|
return;
|
|
85
106
|
// Use a wrapper to call the current handleShake
|
|
86
107
|
shakeDetectorRef.current = new ShakeDetector_1.ShakeDetector(() => handleShakeCallback.current());
|
|
@@ -88,7 +109,64 @@ const IInstall = ({ apiKey, apiEndpoint = 'https://iinstall.app', children, enab
|
|
|
88
109
|
return () => {
|
|
89
110
|
shakeDetectorRef.current?.stop();
|
|
90
111
|
};
|
|
91
|
-
}, [enabled]);
|
|
112
|
+
}, [enabled, isEmulator]);
|
|
113
|
+
(0, react_1.useEffect)(() => {
|
|
114
|
+
if (!enabled || !autoRegisterPushToken || !pushToken)
|
|
115
|
+
return;
|
|
116
|
+
if (lastRegisteredPushTokenRef.current === pushToken)
|
|
117
|
+
return;
|
|
118
|
+
let cancelled = false;
|
|
119
|
+
const syncPushToken = async () => {
|
|
120
|
+
const deviceUdid = await react_native_device_info_1.default.getUniqueId().catch(() => undefined);
|
|
121
|
+
const result = await (0, pushRegistration_1.registerPushToken)({
|
|
122
|
+
token: pushToken,
|
|
123
|
+
apiKey,
|
|
124
|
+
apiEndpoint,
|
|
125
|
+
deviceUdid,
|
|
126
|
+
projectId,
|
|
127
|
+
});
|
|
128
|
+
if (cancelled)
|
|
129
|
+
return;
|
|
130
|
+
if (result.success) {
|
|
131
|
+
lastRegisteredPushTokenRef.current = pushToken;
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
const message = result.error || 'Failed to register push token';
|
|
135
|
+
console.warn('IInstall: push token registration failed', message);
|
|
136
|
+
if (onPushTokenRegisterError) {
|
|
137
|
+
onPushTokenRegisterError(message);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
syncPushToken().catch((error) => {
|
|
142
|
+
if (cancelled)
|
|
143
|
+
return;
|
|
144
|
+
const message = error instanceof Error ? error.message : 'Failed to register push token';
|
|
145
|
+
console.warn('IInstall: push token registration failed', message);
|
|
146
|
+
if (onPushTokenRegisterError) {
|
|
147
|
+
onPushTokenRegisterError(message);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
return () => {
|
|
151
|
+
cancelled = true;
|
|
152
|
+
};
|
|
153
|
+
}, [
|
|
154
|
+
enabled,
|
|
155
|
+
autoRegisterPushToken,
|
|
156
|
+
pushToken,
|
|
157
|
+
apiKey,
|
|
158
|
+
apiEndpoint,
|
|
159
|
+
projectId,
|
|
160
|
+
onPushTokenRegisterError,
|
|
161
|
+
]);
|
|
162
|
+
const handleFloatingButtonPress = async () => {
|
|
163
|
+
await handleShakeCallback.current();
|
|
164
|
+
};
|
|
165
|
+
const shouldShowFloatingButton = enabled &&
|
|
166
|
+
isEmulator &&
|
|
167
|
+
showFloatingButtonOnEmulator &&
|
|
168
|
+
!modalVisible &&
|
|
169
|
+
!isRecording;
|
|
92
170
|
const normalizeVideoUri = (value) => {
|
|
93
171
|
if (react_native_1.Platform.OS === 'ios' && value.startsWith('/')) {
|
|
94
172
|
return `file://${value}`;
|
|
@@ -173,6 +251,12 @@ const IInstall = ({ apiKey, apiEndpoint = 'https://iinstall.app', children, enab
|
|
|
173
251
|
<react_native_1.View style={react_native_1.StyleSheet.absoluteFill}>
|
|
174
252
|
{children}
|
|
175
253
|
</react_native_1.View>
|
|
254
|
+
|
|
255
|
+
{shouldShowFloatingButton && (<react_native_1.View style={styles.floatingButtonContainer} pointerEvents="box-none">
|
|
256
|
+
<react_native_1.TouchableOpacity accessibilityRole="button" accessibilityLabel="Open feedback menu" onPress={handleFloatingButtonPress} style={styles.floatingButton}>
|
|
257
|
+
<react_native_1.Text style={styles.floatingButtonText}>{floatingButtonLabel}</react_native_1.Text>
|
|
258
|
+
</react_native_1.TouchableOpacity>
|
|
259
|
+
</react_native_1.View>)}
|
|
176
260
|
|
|
177
261
|
{isRecording && (<react_native_1.SafeAreaView style={styles.recordingOverlay} pointerEvents="box-none">
|
|
178
262
|
<react_native_1.View style={styles.recordingContainer}>
|
|
@@ -193,6 +277,30 @@ const IInstall = ({ apiKey, apiEndpoint = 'https://iinstall.app', children, enab
|
|
|
193
277
|
};
|
|
194
278
|
exports.IInstall = IInstall;
|
|
195
279
|
const styles = react_native_1.StyleSheet.create({
|
|
280
|
+
floatingButtonContainer: {
|
|
281
|
+
position: 'absolute',
|
|
282
|
+
right: 16,
|
|
283
|
+
bottom: 28,
|
|
284
|
+
zIndex: 9998,
|
|
285
|
+
elevation: 20,
|
|
286
|
+
},
|
|
287
|
+
floatingButton: {
|
|
288
|
+
backgroundColor: '#0f172a',
|
|
289
|
+
borderColor: '#2563eb',
|
|
290
|
+
borderWidth: 1,
|
|
291
|
+
borderRadius: 24,
|
|
292
|
+
paddingHorizontal: 14,
|
|
293
|
+
paddingVertical: 10,
|
|
294
|
+
shadowColor: '#000',
|
|
295
|
+
shadowOffset: { width: 0, height: 2 },
|
|
296
|
+
shadowOpacity: 0.35,
|
|
297
|
+
shadowRadius: 4,
|
|
298
|
+
},
|
|
299
|
+
floatingButtonText: {
|
|
300
|
+
color: '#FFF',
|
|
301
|
+
fontWeight: '700',
|
|
302
|
+
fontSize: 13,
|
|
303
|
+
},
|
|
196
304
|
recordingOverlay: {
|
|
197
305
|
position: 'absolute',
|
|
198
306
|
top: 50,
|
|
@@ -235,3 +343,6 @@ exports.default = exports.IInstall;
|
|
|
235
343
|
// Export the wrapper component for simulator/emulator support
|
|
236
344
|
var IInstallWrapper_1 = require("./IInstallWrapper");
|
|
237
345
|
Object.defineProperty(exports, "IInstallWrapper", { enumerable: true, get: function () { return IInstallWrapper_1.IInstallWrapper; } });
|
|
346
|
+
var pushRegistration_2 = require("./pushRegistration");
|
|
347
|
+
Object.defineProperty(exports, "registerPushToken", { enumerable: true, get: function () { return pushRegistration_2.registerPushToken; } });
|
|
348
|
+
Object.defineProperty(exports, "unregisterPushToken", { enumerable: true, get: function () { return pushRegistration_2.unregisterPushToken; } });
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type PushPlatform = 'IOS' | 'ANDROID';
|
|
2
|
+
export interface RegisterPushTokenParams {
|
|
3
|
+
token: string;
|
|
4
|
+
apiKey: string;
|
|
5
|
+
apiEndpoint?: string;
|
|
6
|
+
deviceUdid?: string;
|
|
7
|
+
projectId?: string;
|
|
8
|
+
platform?: PushPlatform;
|
|
9
|
+
}
|
|
10
|
+
export interface UnregisterPushTokenParams {
|
|
11
|
+
token: string;
|
|
12
|
+
apiKey: string;
|
|
13
|
+
apiEndpoint?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface PushRegistrationResult {
|
|
16
|
+
success: boolean;
|
|
17
|
+
data?: any;
|
|
18
|
+
error?: string;
|
|
19
|
+
status: number;
|
|
20
|
+
}
|
|
21
|
+
export declare function registerPushToken({ token, apiKey, apiEndpoint, deviceUdid, projectId, platform, }: RegisterPushTokenParams): Promise<PushRegistrationResult>;
|
|
22
|
+
export declare function unregisterPushToken({ token, apiKey, apiEndpoint, }: UnregisterPushTokenParams): Promise<PushRegistrationResult>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.registerPushToken = registerPushToken;
|
|
4
|
+
exports.unregisterPushToken = unregisterPushToken;
|
|
5
|
+
const react_native_1 = require("react-native");
|
|
6
|
+
function normalizeApiEndpoint(apiEndpoint) {
|
|
7
|
+
return apiEndpoint.replace(/\/+$/, '');
|
|
8
|
+
}
|
|
9
|
+
function resolvePlatform(platform) {
|
|
10
|
+
if (platform)
|
|
11
|
+
return platform;
|
|
12
|
+
return react_native_1.Platform.OS === 'ios' ? 'IOS' : 'ANDROID';
|
|
13
|
+
}
|
|
14
|
+
async function registerPushToken({ token, apiKey, apiEndpoint = 'https://iinstall.app', deviceUdid, projectId, platform, }) {
|
|
15
|
+
try {
|
|
16
|
+
const response = await fetch(`${normalizeApiEndpoint(apiEndpoint)}/api/notifications/push/register`, {
|
|
17
|
+
method: 'POST',
|
|
18
|
+
headers: { 'Content-Type': 'application/json' },
|
|
19
|
+
body: JSON.stringify({
|
|
20
|
+
token,
|
|
21
|
+
apiKey,
|
|
22
|
+
deviceUdid,
|
|
23
|
+
projectId,
|
|
24
|
+
platform: resolvePlatform(platform),
|
|
25
|
+
}),
|
|
26
|
+
});
|
|
27
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
28
|
+
const data = await response.json().catch(() => ({}));
|
|
29
|
+
return {
|
|
30
|
+
success: response.ok,
|
|
31
|
+
status: response.status,
|
|
32
|
+
data,
|
|
33
|
+
error: response.ok ? undefined : data?.error || 'Failed to register push token',
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
return {
|
|
38
|
+
success: false,
|
|
39
|
+
status: 500,
|
|
40
|
+
error: error instanceof Error ? error.message : 'Unknown register push error',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function unregisterPushToken({ token, apiKey, apiEndpoint = 'https://iinstall.app', }) {
|
|
45
|
+
try {
|
|
46
|
+
const response = await fetch(`${normalizeApiEndpoint(apiEndpoint)}/api/notifications/push/register`, {
|
|
47
|
+
method: 'DELETE',
|
|
48
|
+
headers: { 'Content-Type': 'application/json' },
|
|
49
|
+
body: JSON.stringify({
|
|
50
|
+
token,
|
|
51
|
+
apiKey,
|
|
52
|
+
}),
|
|
53
|
+
});
|
|
54
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
55
|
+
const data = await response.json().catch(() => ({}));
|
|
56
|
+
return {
|
|
57
|
+
success: response.ok,
|
|
58
|
+
status: response.status,
|
|
59
|
+
data,
|
|
60
|
+
error: response.ok ? undefined : data?.error || 'Failed to unregister push token',
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
return {
|
|
65
|
+
success: false,
|
|
66
|
+
status: 500,
|
|
67
|
+
error: error instanceof Error ? error.message : 'Unknown unregister push error',
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-iinstall",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.13",
|
|
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",
|
package/src/IInstallWrapper.tsx
CHANGED
|
@@ -1,178 +1,52 @@
|
|
|
1
|
-
import 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
|
+
pushToken?: string;
|
|
14
|
+
autoRegisterPushToken?: boolean;
|
|
15
|
+
projectId?: string;
|
|
16
|
+
onPushTokenRegisterError?: (error: string) => void;
|
|
13
17
|
}
|
|
14
18
|
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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',
|
|
19
|
+
// Backward-compatible wrapper. The core IInstall component now handles:
|
|
20
|
+
// - shake gesture on real devices
|
|
21
|
+
// - floating manual trigger button on simulator/emulator
|
|
22
|
+
export const IInstallWrapper: React.FC<IInstallWrapperProps> = ({
|
|
23
|
+
apiKey,
|
|
24
|
+
apiEndpoint = 'https://iinstall.app',
|
|
56
25
|
children,
|
|
57
26
|
enabled = true,
|
|
58
|
-
showDebugButton =
|
|
27
|
+
showDebugButton: _showDebugButton = false,
|
|
28
|
+
showFloatingButtonOnEmulator = true,
|
|
29
|
+
floatingButtonLabel = 'Report Issue',
|
|
30
|
+
pushToken,
|
|
31
|
+
autoRegisterPushToken = true,
|
|
32
|
+
projectId,
|
|
33
|
+
onPushTokenRegisterError,
|
|
59
34
|
}) => {
|
|
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
35
|
return (
|
|
106
|
-
<IInstall
|
|
107
|
-
apiKey={apiKey}
|
|
108
|
-
apiEndpoint={apiEndpoint}
|
|
36
|
+
<IInstall
|
|
37
|
+
apiKey={apiKey}
|
|
38
|
+
apiEndpoint={apiEndpoint}
|
|
109
39
|
enabled={enabled}
|
|
40
|
+
showFloatingButtonOnEmulator={showFloatingButtonOnEmulator}
|
|
41
|
+
floatingButtonLabel={floatingButtonLabel}
|
|
42
|
+
pushToken={pushToken}
|
|
43
|
+
autoRegisterPushToken={autoRegisterPushToken}
|
|
44
|
+
projectId={projectId}
|
|
45
|
+
onPushTokenRegisterError={onPushTokenRegisterError}
|
|
110
46
|
>
|
|
111
47
|
{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
48
|
</IInstall>
|
|
133
49
|
);
|
|
134
50
|
};
|
|
135
51
|
|
|
136
|
-
|
|
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
|
-
});
|
|
52
|
+
export default IInstallWrapper;
|
package/src/index.tsx
CHANGED
|
@@ -4,26 +4,42 @@ 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';
|
|
8
|
+
import { registerPushToken } from './pushRegistration';
|
|
7
9
|
|
|
8
10
|
interface IInstallProps {
|
|
9
11
|
apiKey: string;
|
|
10
12
|
apiEndpoint?: string; // e.g. https://iinstall.app
|
|
11
13
|
children?: React.ReactNode;
|
|
12
14
|
enabled?: boolean;
|
|
15
|
+
showFloatingButtonOnEmulator?: boolean;
|
|
16
|
+
floatingButtonLabel?: string;
|
|
17
|
+
pushToken?: string;
|
|
18
|
+
autoRegisterPushToken?: boolean;
|
|
19
|
+
projectId?: string;
|
|
20
|
+
onPushTokenRegisterError?: (error: string) => void;
|
|
13
21
|
}
|
|
14
22
|
|
|
15
23
|
export const IInstall: React.FC<IInstallProps> = ({
|
|
16
24
|
apiKey,
|
|
17
25
|
apiEndpoint = 'https://iinstall.app',
|
|
18
26
|
children,
|
|
19
|
-
enabled = true
|
|
27
|
+
enabled = true,
|
|
28
|
+
showFloatingButtonOnEmulator = true,
|
|
29
|
+
floatingButtonLabel = 'Report Issue',
|
|
30
|
+
pushToken,
|
|
31
|
+
autoRegisterPushToken = true,
|
|
32
|
+
projectId,
|
|
33
|
+
onPushTokenRegisterError,
|
|
20
34
|
}) => {
|
|
21
35
|
const [modalVisible, setModalVisible] = useState(false);
|
|
22
36
|
const [screenshotUri, setScreenshotUri] = useState<string | null>(null);
|
|
23
37
|
const [videoUri, setVideoUri] = useState<string | null>(null);
|
|
24
38
|
const [isRecording, setIsRecording] = useState(false);
|
|
39
|
+
const [isEmulator, setIsEmulator] = useState(false);
|
|
25
40
|
|
|
26
41
|
const shakeDetectorRef = useRef<ShakeDetector | null>(null);
|
|
42
|
+
const lastRegisteredPushTokenRef = useRef<string | null>(null);
|
|
27
43
|
|
|
28
44
|
// Refs for stable access in shake callback
|
|
29
45
|
const isRecordingRef = useRef(isRecording);
|
|
@@ -34,6 +50,26 @@ export const IInstall: React.FC<IInstallProps> = ({
|
|
|
34
50
|
modalVisibleRef.current = modalVisible;
|
|
35
51
|
}, [isRecording, modalVisible]);
|
|
36
52
|
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
let mounted = true;
|
|
55
|
+
|
|
56
|
+
DeviceInfo.isEmulator()
|
|
57
|
+
.then((value) => {
|
|
58
|
+
if (mounted) {
|
|
59
|
+
setIsEmulator(value);
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
.catch(() => {
|
|
63
|
+
if (mounted) {
|
|
64
|
+
setIsEmulator(false);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return () => {
|
|
69
|
+
mounted = false;
|
|
70
|
+
};
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
37
73
|
const handleShake = async () => {
|
|
38
74
|
if (modalVisibleRef.current || isRecordingRef.current) return;
|
|
39
75
|
|
|
@@ -59,7 +95,7 @@ export const IInstall: React.FC<IInstallProps> = ({
|
|
|
59
95
|
});
|
|
60
96
|
|
|
61
97
|
useEffect(() => {
|
|
62
|
-
if (!enabled) return;
|
|
98
|
+
if (!enabled || isEmulator) return;
|
|
63
99
|
|
|
64
100
|
// Use a wrapper to call the current handleShake
|
|
65
101
|
shakeDetectorRef.current = new ShakeDetector(() => handleShakeCallback.current());
|
|
@@ -68,7 +104,70 @@ export const IInstall: React.FC<IInstallProps> = ({
|
|
|
68
104
|
return () => {
|
|
69
105
|
shakeDetectorRef.current?.stop();
|
|
70
106
|
};
|
|
71
|
-
}, [enabled]);
|
|
107
|
+
}, [enabled, isEmulator]);
|
|
108
|
+
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (!enabled || !autoRegisterPushToken || !pushToken) return;
|
|
111
|
+
if (lastRegisteredPushTokenRef.current === pushToken) return;
|
|
112
|
+
|
|
113
|
+
let cancelled = false;
|
|
114
|
+
|
|
115
|
+
const syncPushToken = async () => {
|
|
116
|
+
const deviceUdid = await DeviceInfo.getUniqueId().catch(() => undefined);
|
|
117
|
+
const result = await registerPushToken({
|
|
118
|
+
token: pushToken,
|
|
119
|
+
apiKey,
|
|
120
|
+
apiEndpoint,
|
|
121
|
+
deviceUdid,
|
|
122
|
+
projectId,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
if (cancelled) return;
|
|
126
|
+
|
|
127
|
+
if (result.success) {
|
|
128
|
+
lastRegisteredPushTokenRef.current = pushToken;
|
|
129
|
+
} else {
|
|
130
|
+
const message = result.error || 'Failed to register push token';
|
|
131
|
+
console.warn('IInstall: push token registration failed', message);
|
|
132
|
+
if (onPushTokenRegisterError) {
|
|
133
|
+
onPushTokenRegisterError(message);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
syncPushToken().catch((error) => {
|
|
139
|
+
if (cancelled) return;
|
|
140
|
+
const message =
|
|
141
|
+
error instanceof Error ? error.message : 'Failed to register push token';
|
|
142
|
+
console.warn('IInstall: push token registration failed', message);
|
|
143
|
+
if (onPushTokenRegisterError) {
|
|
144
|
+
onPushTokenRegisterError(message);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return () => {
|
|
149
|
+
cancelled = true;
|
|
150
|
+
};
|
|
151
|
+
}, [
|
|
152
|
+
enabled,
|
|
153
|
+
autoRegisterPushToken,
|
|
154
|
+
pushToken,
|
|
155
|
+
apiKey,
|
|
156
|
+
apiEndpoint,
|
|
157
|
+
projectId,
|
|
158
|
+
onPushTokenRegisterError,
|
|
159
|
+
]);
|
|
160
|
+
|
|
161
|
+
const handleFloatingButtonPress = async () => {
|
|
162
|
+
await handleShakeCallback.current();
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const shouldShowFloatingButton =
|
|
166
|
+
enabled &&
|
|
167
|
+
isEmulator &&
|
|
168
|
+
showFloatingButtonOnEmulator &&
|
|
169
|
+
!modalVisible &&
|
|
170
|
+
!isRecording;
|
|
72
171
|
|
|
73
172
|
const normalizeVideoUri = (value: string) => {
|
|
74
173
|
if (Platform.OS === 'ios' && value.startsWith('/')) {
|
|
@@ -181,6 +280,19 @@ export const IInstall: React.FC<IInstallProps> = ({
|
|
|
181
280
|
<View style={StyleSheet.absoluteFill}>
|
|
182
281
|
{children}
|
|
183
282
|
</View>
|
|
283
|
+
|
|
284
|
+
{shouldShowFloatingButton && (
|
|
285
|
+
<View style={styles.floatingButtonContainer} pointerEvents="box-none">
|
|
286
|
+
<TouchableOpacity
|
|
287
|
+
accessibilityRole="button"
|
|
288
|
+
accessibilityLabel="Open feedback menu"
|
|
289
|
+
onPress={handleFloatingButtonPress}
|
|
290
|
+
style={styles.floatingButton}
|
|
291
|
+
>
|
|
292
|
+
<Text style={styles.floatingButtonText}>{floatingButtonLabel}</Text>
|
|
293
|
+
</TouchableOpacity>
|
|
294
|
+
</View>
|
|
295
|
+
)}
|
|
184
296
|
|
|
185
297
|
{isRecording && (
|
|
186
298
|
<SafeAreaView style={styles.recordingOverlay} pointerEvents="box-none">
|
|
@@ -212,6 +324,30 @@ export const IInstall: React.FC<IInstallProps> = ({
|
|
|
212
324
|
};
|
|
213
325
|
|
|
214
326
|
const styles = StyleSheet.create({
|
|
327
|
+
floatingButtonContainer: {
|
|
328
|
+
position: 'absolute',
|
|
329
|
+
right: 16,
|
|
330
|
+
bottom: 28,
|
|
331
|
+
zIndex: 9998,
|
|
332
|
+
elevation: 20,
|
|
333
|
+
},
|
|
334
|
+
floatingButton: {
|
|
335
|
+
backgroundColor: '#0f172a',
|
|
336
|
+
borderColor: '#2563eb',
|
|
337
|
+
borderWidth: 1,
|
|
338
|
+
borderRadius: 24,
|
|
339
|
+
paddingHorizontal: 14,
|
|
340
|
+
paddingVertical: 10,
|
|
341
|
+
shadowColor: '#000',
|
|
342
|
+
shadowOffset: { width: 0, height: 2 },
|
|
343
|
+
shadowOpacity: 0.35,
|
|
344
|
+
shadowRadius: 4,
|
|
345
|
+
},
|
|
346
|
+
floatingButtonText: {
|
|
347
|
+
color: '#FFF',
|
|
348
|
+
fontWeight: '700',
|
|
349
|
+
fontSize: 13,
|
|
350
|
+
},
|
|
215
351
|
recordingOverlay: {
|
|
216
352
|
position: 'absolute',
|
|
217
353
|
top: 50,
|
|
@@ -255,3 +391,11 @@ export default IInstall;
|
|
|
255
391
|
|
|
256
392
|
// Export the wrapper component for simulator/emulator support
|
|
257
393
|
export { IInstallWrapper } from './IInstallWrapper';
|
|
394
|
+
export {
|
|
395
|
+
registerPushToken,
|
|
396
|
+
unregisterPushToken,
|
|
397
|
+
type RegisterPushTokenParams,
|
|
398
|
+
type UnregisterPushTokenParams,
|
|
399
|
+
type PushRegistrationResult,
|
|
400
|
+
type PushPlatform,
|
|
401
|
+
} from './pushRegistration';
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Platform } from 'react-native';
|
|
2
|
+
|
|
3
|
+
export type PushPlatform = 'IOS' | 'ANDROID';
|
|
4
|
+
|
|
5
|
+
export interface RegisterPushTokenParams {
|
|
6
|
+
token: string;
|
|
7
|
+
apiKey: string;
|
|
8
|
+
apiEndpoint?: string;
|
|
9
|
+
deviceUdid?: string;
|
|
10
|
+
projectId?: string;
|
|
11
|
+
platform?: PushPlatform;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface UnregisterPushTokenParams {
|
|
15
|
+
token: string;
|
|
16
|
+
apiKey: string;
|
|
17
|
+
apiEndpoint?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PushRegistrationResult {
|
|
21
|
+
success: boolean;
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
23
|
+
data?: any;
|
|
24
|
+
error?: string;
|
|
25
|
+
status: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeApiEndpoint(apiEndpoint: string): string {
|
|
29
|
+
return apiEndpoint.replace(/\/+$/, '');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function resolvePlatform(platform?: PushPlatform): PushPlatform {
|
|
33
|
+
if (platform) return platform;
|
|
34
|
+
return Platform.OS === 'ios' ? 'IOS' : 'ANDROID';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function registerPushToken({
|
|
38
|
+
token,
|
|
39
|
+
apiKey,
|
|
40
|
+
apiEndpoint = 'https://iinstall.app',
|
|
41
|
+
deviceUdid,
|
|
42
|
+
projectId,
|
|
43
|
+
platform,
|
|
44
|
+
}: RegisterPushTokenParams): Promise<PushRegistrationResult> {
|
|
45
|
+
try {
|
|
46
|
+
const response = await fetch(
|
|
47
|
+
`${normalizeApiEndpoint(apiEndpoint)}/api/notifications/push/register`,
|
|
48
|
+
{
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: { 'Content-Type': 'application/json' },
|
|
51
|
+
body: JSON.stringify({
|
|
52
|
+
token,
|
|
53
|
+
apiKey,
|
|
54
|
+
deviceUdid,
|
|
55
|
+
projectId,
|
|
56
|
+
platform: resolvePlatform(platform),
|
|
57
|
+
}),
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
62
|
+
const data: any = await response.json().catch(() => ({}));
|
|
63
|
+
return {
|
|
64
|
+
success: response.ok,
|
|
65
|
+
status: response.status,
|
|
66
|
+
data,
|
|
67
|
+
error: response.ok ? undefined : data?.error || 'Failed to register push token',
|
|
68
|
+
};
|
|
69
|
+
} catch (error) {
|
|
70
|
+
return {
|
|
71
|
+
success: false,
|
|
72
|
+
status: 500,
|
|
73
|
+
error: error instanceof Error ? error.message : 'Unknown register push error',
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function unregisterPushToken({
|
|
79
|
+
token,
|
|
80
|
+
apiKey,
|
|
81
|
+
apiEndpoint = 'https://iinstall.app',
|
|
82
|
+
}: UnregisterPushTokenParams): Promise<PushRegistrationResult> {
|
|
83
|
+
try {
|
|
84
|
+
const response = await fetch(
|
|
85
|
+
`${normalizeApiEndpoint(apiEndpoint)}/api/notifications/push/register`,
|
|
86
|
+
{
|
|
87
|
+
method: 'DELETE',
|
|
88
|
+
headers: { 'Content-Type': 'application/json' },
|
|
89
|
+
body: JSON.stringify({
|
|
90
|
+
token,
|
|
91
|
+
apiKey,
|
|
92
|
+
}),
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
97
|
+
const data: any = await response.json().catch(() => ({}));
|
|
98
|
+
return {
|
|
99
|
+
success: response.ok,
|
|
100
|
+
status: response.status,
|
|
101
|
+
data,
|
|
102
|
+
error: response.ok ? undefined : data?.error || 'Failed to unregister push token',
|
|
103
|
+
};
|
|
104
|
+
} catch (error) {
|
|
105
|
+
return {
|
|
106
|
+
success: false,
|
|
107
|
+
status: 500,
|
|
108
|
+
error: error instanceof Error ? error.message : 'Unknown unregister push error',
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|