@mindedsolutions/bug-reporter-sdk 0.1.2 → 0.3.1
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/dist/components/FeatureBoardLink.d.ts +8 -0
- package/dist/components/FeatureBoardLink.js +28 -0
- package/dist/components/ReportModal.js +12 -11
- package/dist/hooks/useFeatureBoard.d.ts +4 -0
- package/dist/hooks/useFeatureBoard.js +22 -0
- package/dist/hooks/useScreenCapture.d.ts +4 -2
- package/dist/hooks/useScreenCapture.js +49 -22
- package/dist/i18n/en.js +2 -0
- package/dist/i18n/fr.js +2 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +5 -1
- package/dist/types.d.ts +8 -0
- package/package.json +2 -2
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ViewStyle, TextStyle } from 'react-native';
|
|
2
|
+
interface FeatureBoardLinkProps {
|
|
3
|
+
style?: ViewStyle;
|
|
4
|
+
textStyle?: TextStyle;
|
|
5
|
+
label?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function FeatureBoardLink({ style, textStyle, label }: FeatureBoardLinkProps): import("react/jsx-runtime").JSX.Element | null;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FeatureBoardLink = FeatureBoardLink;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const react_native_1 = require("react-native");
|
|
6
|
+
const useFeatureBoard_1 = require("../hooks/useFeatureBoard");
|
|
7
|
+
const useBugReporter_1 = require("../hooks/useBugReporter");
|
|
8
|
+
function FeatureBoardLink({ style, textStyle, label }) {
|
|
9
|
+
const { boardUrl, openFeatureBoard } = (0, useFeatureBoard_1.useFeatureBoard)();
|
|
10
|
+
const { translations } = (0, useBugReporter_1.useBugReporter)();
|
|
11
|
+
if (!boardUrl)
|
|
12
|
+
return null;
|
|
13
|
+
return ((0, jsx_runtime_1.jsx)(react_native_1.TouchableOpacity, { onPress: openFeatureBoard, style: [styles.button, style], activeOpacity: 0.8, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.text, textStyle], children: label ?? translations.suggestFeature }) }));
|
|
14
|
+
}
|
|
15
|
+
const styles = react_native_1.StyleSheet.create({
|
|
16
|
+
button: {
|
|
17
|
+
backgroundColor: '#4f46e5',
|
|
18
|
+
paddingHorizontal: 20,
|
|
19
|
+
paddingVertical: 12,
|
|
20
|
+
borderRadius: 10,
|
|
21
|
+
alignItems: 'center',
|
|
22
|
+
},
|
|
23
|
+
text: {
|
|
24
|
+
color: '#ffffff',
|
|
25
|
+
fontSize: 15,
|
|
26
|
+
fontWeight: '600',
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -12,18 +12,24 @@ const ScreenshotPreview_1 = require("./ScreenshotPreview");
|
|
|
12
12
|
const constants_1 = require("../constants");
|
|
13
13
|
function ReportModal() {
|
|
14
14
|
const { config, translations, isModalVisible, closeModal } = (0, useBugReporter_1.useBugReporter)();
|
|
15
|
-
const {
|
|
15
|
+
const { captureAndUpload } = (0, useScreenCapture_1.useScreenCapture)();
|
|
16
16
|
const { getDeviceInfo, getAppInfo, getNetworkInfo } = (0, useDeviceInfo_1.useDeviceInfo)();
|
|
17
17
|
const [description, setDescription] = (0, react_1.useState)('');
|
|
18
18
|
const [category, setCategory] = (0, react_1.useState)(config.defaultCategory ?? 'Bug');
|
|
19
19
|
const [severity, setSeverity] = (0, react_1.useState)();
|
|
20
20
|
const [screenshotUri, setScreenshotUri] = (0, react_1.useState)(null);
|
|
21
|
+
const [screenshotUrl, setScreenshotUrl] = (0, react_1.useState)(null);
|
|
21
22
|
const [isSubmitting, setIsSubmitting] = (0, react_1.useState)(false);
|
|
22
23
|
const categories = config.categories ?? constants_1.DEFAULT_CATEGORIES;
|
|
23
|
-
// Capture screenshot when modal opens
|
|
24
|
+
// Capture screenshot and upload when modal opens
|
|
24
25
|
(0, react_1.useEffect)(() => {
|
|
25
26
|
if (isModalVisible) {
|
|
26
|
-
|
|
27
|
+
captureAndUpload().then((result) => {
|
|
28
|
+
if (result) {
|
|
29
|
+
setScreenshotUri(result.uri);
|
|
30
|
+
setScreenshotUrl(result.url || null);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
27
33
|
}
|
|
28
34
|
else {
|
|
29
35
|
// Reset form when modal closes
|
|
@@ -31,6 +37,7 @@ function ReportModal() {
|
|
|
31
37
|
setCategory(config.defaultCategory ?? 'Bug');
|
|
32
38
|
setSeverity(undefined);
|
|
33
39
|
setScreenshotUri(null);
|
|
40
|
+
setScreenshotUrl(null);
|
|
34
41
|
setIsSubmitting(false);
|
|
35
42
|
}
|
|
36
43
|
}, [isModalVisible]);
|
|
@@ -42,17 +49,11 @@ function ReportModal() {
|
|
|
42
49
|
const device = getDeviceInfo();
|
|
43
50
|
const app = getAppInfo();
|
|
44
51
|
const network = await getNetworkInfo();
|
|
45
|
-
let screenshotUrl;
|
|
46
|
-
if (screenshotUri) {
|
|
47
|
-
const url = await uploadScreenshot(screenshotUri);
|
|
48
|
-
if (url)
|
|
49
|
-
screenshotUrl = url;
|
|
50
|
-
}
|
|
51
52
|
const payload = {
|
|
52
53
|
description: description.trim(),
|
|
53
54
|
category,
|
|
54
55
|
severity,
|
|
55
|
-
screenshot_url: screenshotUrl,
|
|
56
|
+
screenshot_url: screenshotUrl || undefined,
|
|
56
57
|
device_brand: device.brand,
|
|
57
58
|
device_model: device.model,
|
|
58
59
|
device_os: device.os,
|
|
@@ -82,7 +83,7 @@ function ReportModal() {
|
|
|
82
83
|
finally {
|
|
83
84
|
setIsSubmitting(false);
|
|
84
85
|
}
|
|
85
|
-
}, [description, category, severity,
|
|
86
|
+
}, [description, category, severity, screenshotUrl, config, translations, closeModal]);
|
|
86
87
|
return ((0, jsx_runtime_1.jsx)(react_native_1.Modal, { visible: isModalVisible, animationType: "slide", presentationStyle: "fullScreen", onRequestClose: closeModal, children: (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.container, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.safeTop }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.header, children: [(0, jsx_runtime_1.jsx)(react_native_1.TouchableOpacity, { onPress: closeModal, disabled: isSubmitting, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.cancelText, children: translations.cancel }) }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.title, children: translations.reportBug }), (0, jsx_runtime_1.jsx)(react_native_1.TouchableOpacity, { onPress: handleSubmit, disabled: isSubmitting || !description.trim(), children: isSubmitting ? ((0, jsx_runtime_1.jsx)(react_native_1.ActivityIndicator, { size: "small", color: "#6366f1" })) : ((0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.submitText, !description.trim() && styles.disabledText], children: translations.submit })) })] }), (0, jsx_runtime_1.jsxs)(react_native_1.ScrollView, { style: styles.body, contentContainerStyle: styles.bodyContent, keyboardShouldPersistTaps: "handled", children: [screenshotUri && ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.section, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.label, children: translations.screenshot }), (0, jsx_runtime_1.jsx)(ScreenshotPreview_1.ScreenshotPreview, { uri: screenshotUri, onRemove: () => setScreenshotUri(null), removeLabel: translations.removeScreenshot })] })), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.section, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.label, children: translations.description }), (0, jsx_runtime_1.jsx)(react_native_1.TextInput, { style: styles.textInput, value: description, onChangeText: setDescription, placeholder: translations.descriptionPlaceholder, placeholderTextColor: "#9ca3af", multiline: true, numberOfLines: 4, textAlignVertical: "top" })] }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.section, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.label, children: translations.category }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.chipRow, children: categories.map((cat) => ((0, jsx_runtime_1.jsx)(react_native_1.TouchableOpacity, { style: [styles.chip, category === cat && styles.chipActive], onPress: () => setCategory(cat), children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: [styles.chipText, category === cat && styles.chipTextActive], children: cat }) }, cat))) })] }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.section, children: [(0, jsx_runtime_1.jsxs)(react_native_1.Text, { style: styles.label, children: [translations.severity, " ", (0, jsx_runtime_1.jsxs)(react_native_1.Text, { style: styles.optional, children: ["(", translations.optional, ")"] })] }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.chipRow, children: constants_1.SEVERITIES.map((sev) => {
|
|
87
88
|
const labels = {
|
|
88
89
|
low: translations.severityLow,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.useFeatureBoard = useFeatureBoard;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
const react_native_1 = require("react-native");
|
|
6
|
+
const useBugReporter_1 = require("./useBugReporter");
|
|
7
|
+
function useFeatureBoard() {
|
|
8
|
+
const { config } = (0, useBugReporter_1.useBugReporter)();
|
|
9
|
+
const boardUrl = (0, react_1.useMemo)(() => {
|
|
10
|
+
if (!config.featureBoard)
|
|
11
|
+
return null;
|
|
12
|
+
const base = config.featureBoard.boardBaseUrl.replace(/\/$/, '');
|
|
13
|
+
const url = `${base}/${config.projectId}`;
|
|
14
|
+
return config.userId ? `${url}?voter_id=${encodeURIComponent(config.userId)}` : url;
|
|
15
|
+
}, [config.featureBoard, config.projectId, config.userId]);
|
|
16
|
+
const openFeatureBoard = (0, react_1.useCallback)(() => {
|
|
17
|
+
if (boardUrl) {
|
|
18
|
+
react_native_1.Linking.openURL(boardUrl);
|
|
19
|
+
}
|
|
20
|
+
}, [boardUrl]);
|
|
21
|
+
return { boardUrl, openFeatureBoard };
|
|
22
|
+
}
|
|
@@ -6,45 +6,72 @@ const react_native_view_shot_1 = require("react-native-view-shot");
|
|
|
6
6
|
const supabase_js_1 = require("@supabase/supabase-js");
|
|
7
7
|
const useBugReporter_1 = require("./useBugReporter");
|
|
8
8
|
const constants_1 = require("../constants");
|
|
9
|
+
function base64ToArrayBuffer(base64) {
|
|
10
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
11
|
+
// Count padding from the original string before stripping
|
|
12
|
+
let padding = 0;
|
|
13
|
+
if (base64.endsWith('=='))
|
|
14
|
+
padding = 2;
|
|
15
|
+
else if (base64.endsWith('='))
|
|
16
|
+
padding = 1;
|
|
17
|
+
// Remove all non-base64 characters (including '=' padding)
|
|
18
|
+
const clean = base64.replace(/[^A-Za-z0-9+/]/g, '');
|
|
19
|
+
const len = clean.length;
|
|
20
|
+
// The byte length formula requires the padded base64 length (multiple of 4).
|
|
21
|
+
// Since clean has '=' stripped, we add padding back for the calculation.
|
|
22
|
+
const paddedLen = len + padding;
|
|
23
|
+
const byteLen = (paddedLen * 3) / 4 - padding;
|
|
24
|
+
const buffer = new ArrayBuffer(byteLen);
|
|
25
|
+
const bytes = new Uint8Array(buffer);
|
|
26
|
+
let p = 0;
|
|
27
|
+
for (let i = 0; i < len; i += 4) {
|
|
28
|
+
const a = chars.indexOf(clean[i]);
|
|
29
|
+
const b = chars.indexOf(clean[i + 1]);
|
|
30
|
+
const c = chars.indexOf(clean[i + 2]);
|
|
31
|
+
const d = chars.indexOf(clean[i + 3]);
|
|
32
|
+
const bits = (a << 18) | (b << 12) | (c << 6) | d;
|
|
33
|
+
bytes[p++] = (bits >> 16) & 0xff;
|
|
34
|
+
if (p < byteLen)
|
|
35
|
+
bytes[p++] = (bits >> 8) & 0xff;
|
|
36
|
+
if (p < byteLen)
|
|
37
|
+
bytes[p++] = bits & 0xff;
|
|
38
|
+
}
|
|
39
|
+
return buffer;
|
|
40
|
+
}
|
|
9
41
|
function useScreenCapture() {
|
|
10
42
|
const { viewRef, config } = (0, useBugReporter_1.useBugReporter)();
|
|
11
|
-
const
|
|
43
|
+
const captureAndUpload = (0, react_1.useCallback)(async () => {
|
|
12
44
|
try {
|
|
13
45
|
if (!viewRef.current)
|
|
14
46
|
return null;
|
|
47
|
+
// Capture as both URI (for preview) and base64 (for upload)
|
|
15
48
|
const uri = await (0, react_native_view_shot_1.captureRef)(viewRef.current, {
|
|
16
49
|
format: constants_1.SCREENSHOT_FORMAT,
|
|
17
50
|
quality: constants_1.SCREENSHOT_QUALITY,
|
|
18
51
|
});
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const uploadScreenshot = (0, react_1.useCallback)(async (uri) => {
|
|
26
|
-
try {
|
|
52
|
+
const base64 = await (0, react_native_view_shot_1.captureRef)(viewRef.current, {
|
|
53
|
+
format: constants_1.SCREENSHOT_FORMAT,
|
|
54
|
+
quality: constants_1.SCREENSHOT_QUALITY,
|
|
55
|
+
result: 'base64',
|
|
56
|
+
});
|
|
57
|
+
// Upload base64 decoded to Supabase Storage
|
|
27
58
|
const supabase = (0, supabase_js_1.createClient)(config.supabaseUrl, config.supabaseAnonKey);
|
|
28
59
|
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
29
60
|
const fileName = `${config.projectId}/${uniqueId}.jpg`;
|
|
30
|
-
|
|
31
|
-
const formData = new FormData();
|
|
32
|
-
formData.append('', {
|
|
33
|
-
uri,
|
|
34
|
-
name: `${uniqueId}.jpg`,
|
|
35
|
-
type: 'image/jpeg',
|
|
36
|
-
});
|
|
61
|
+
const arrayBuffer = base64ToArrayBuffer(base64);
|
|
37
62
|
const { error } = await supabase.storage
|
|
38
63
|
.from('screenshots')
|
|
39
|
-
.upload(fileName,
|
|
40
|
-
if (error)
|
|
41
|
-
return
|
|
64
|
+
.upload(fileName, arrayBuffer, { contentType: 'image/jpeg' });
|
|
65
|
+
if (error) {
|
|
66
|
+
// Upload failed, still return URI for preview but no remote URL
|
|
67
|
+
return { uri, url: '' };
|
|
68
|
+
}
|
|
42
69
|
const { data } = supabase.storage.from('screenshots').getPublicUrl(fileName);
|
|
43
|
-
return data.publicUrl;
|
|
70
|
+
return { uri, url: data.publicUrl };
|
|
44
71
|
}
|
|
45
72
|
catch {
|
|
46
73
|
return null;
|
|
47
74
|
}
|
|
48
|
-
}, [config]);
|
|
49
|
-
return {
|
|
75
|
+
}, [viewRef, config]);
|
|
76
|
+
return { captureAndUpload };
|
|
50
77
|
}
|
package/dist/i18n/en.js
CHANGED
package/dist/i18n/fr.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { BugReporterProvider } from './context/BugReporterProvider';
|
|
2
2
|
export { useBugReporter } from './hooks/useBugReporter';
|
|
3
|
+
export { useFeatureBoard } from './hooks/useFeatureBoard';
|
|
3
4
|
export { FloatingButton } from './components/FloatingButton';
|
|
4
|
-
export
|
|
5
|
+
export { FeatureBoardLink } from './components/FeatureBoardLink';
|
|
6
|
+
export type { BugReporterConfig, BugReportPayload, BugCategory, BugSeverity, BugStatus, SupportedLocale, DeviceInfo, AppInfo, NetworkInfo, Translations, FeatureCategory, FeatureStatus, FeatureBoardConfig, } from './types';
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.FloatingButton = exports.useBugReporter = exports.BugReporterProvider = void 0;
|
|
3
|
+
exports.FeatureBoardLink = exports.FloatingButton = exports.useFeatureBoard = exports.useBugReporter = exports.BugReporterProvider = void 0;
|
|
4
4
|
var BugReporterProvider_1 = require("./context/BugReporterProvider");
|
|
5
5
|
Object.defineProperty(exports, "BugReporterProvider", { enumerable: true, get: function () { return BugReporterProvider_1.BugReporterProvider; } });
|
|
6
6
|
var useBugReporter_1 = require("./hooks/useBugReporter");
|
|
7
7
|
Object.defineProperty(exports, "useBugReporter", { enumerable: true, get: function () { return useBugReporter_1.useBugReporter; } });
|
|
8
|
+
var useFeatureBoard_1 = require("./hooks/useFeatureBoard");
|
|
9
|
+
Object.defineProperty(exports, "useFeatureBoard", { enumerable: true, get: function () { return useFeatureBoard_1.useFeatureBoard; } });
|
|
8
10
|
var FloatingButton_1 = require("./components/FloatingButton");
|
|
9
11
|
Object.defineProperty(exports, "FloatingButton", { enumerable: true, get: function () { return FloatingButton_1.FloatingButton; } });
|
|
12
|
+
var FeatureBoardLink_1 = require("./components/FeatureBoardLink");
|
|
13
|
+
Object.defineProperty(exports, "FeatureBoardLink", { enumerable: true, get: function () { return FeatureBoardLink_1.FeatureBoardLink; } });
|
package/dist/types.d.ts
CHANGED
|
@@ -2,6 +2,11 @@ export type BugCategory = 'Bug' | 'Crash' | 'UI' | 'Performance' | 'Feature Requ
|
|
|
2
2
|
export type BugSeverity = 'low' | 'medium' | 'high' | 'critical';
|
|
3
3
|
export type BugStatus = 'open' | 'in_progress' | 'resolved' | 'closed';
|
|
4
4
|
export type SupportedLocale = 'en' | 'fr';
|
|
5
|
+
export type FeatureCategory = 'UI/UX' | 'Performance' | 'New Feature' | 'Integration' | 'Improvement' | 'Other';
|
|
6
|
+
export type FeatureStatus = 'under_review' | 'planned' | 'in_progress' | 'completed' | 'declined';
|
|
7
|
+
export interface FeatureBoardConfig {
|
|
8
|
+
boardBaseUrl: string;
|
|
9
|
+
}
|
|
5
10
|
export interface DeviceInfo {
|
|
6
11
|
brand: string | null;
|
|
7
12
|
model: string | null;
|
|
@@ -51,6 +56,7 @@ export interface BugReporterConfig {
|
|
|
51
56
|
currentScreen?: string;
|
|
52
57
|
userId?: string;
|
|
53
58
|
customData?: Record<string, unknown>;
|
|
59
|
+
featureBoard?: FeatureBoardConfig;
|
|
54
60
|
}
|
|
55
61
|
export interface Translations {
|
|
56
62
|
reportBug: string;
|
|
@@ -70,4 +76,6 @@ export interface Translations {
|
|
|
70
76
|
severityMedium: string;
|
|
71
77
|
severityHigh: string;
|
|
72
78
|
severityCritical: string;
|
|
79
|
+
featureBoard: string;
|
|
80
|
+
suggestFeature: string;
|
|
73
81
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mindedsolutions/bug-reporter-sdk",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "In-app bug reporting SDK for React Native/Expo with shake detection, screenshot capture, and Supabase integration",
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "In-app bug reporting and feature request SDK for React Native/Expo with shake detection, screenshot capture, feature board, and Supabase integration",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"license": "MIT",
|