@mindedsolutions/bug-reporter-sdk 0.3.9 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ReportModal.js +95 -9
- package/dist/context/BugReporterContext.d.ts +5 -0
- package/dist/context/BugReporterProvider.js +46 -3
- package/dist/hooks/useScreenCapture.js +9 -40
- package/dist/i18n/en.js +1 -0
- package/dist/i18n/fr.js +1 -0
- package/dist/types.d.ts +2 -1
- package/dist/utils/base64.d.ts +1 -0
- package/dist/utils/base64.js +35 -0
- package/package.json +3 -1
|
@@ -1,17 +1,52 @@
|
|
|
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
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.ReportModal = ReportModal;
|
|
4
37
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
38
|
const react_1 = require("react");
|
|
6
39
|
const react_native_1 = require("react-native");
|
|
7
40
|
const supabase_js_1 = require("@supabase/supabase-js");
|
|
41
|
+
const ImagePicker = __importStar(require("expo-image-picker"));
|
|
8
42
|
const useBugReporter_1 = require("../hooks/useBugReporter");
|
|
9
43
|
const useScreenCapture_1 = require("../hooks/useScreenCapture");
|
|
10
44
|
const useDeviceInfo_1 = require("../hooks/useDeviceInfo");
|
|
11
45
|
const ScreenshotPreview_1 = require("./ScreenshotPreview");
|
|
12
46
|
const constants_1 = require("../constants");
|
|
47
|
+
const base64_1 = require("../utils/base64");
|
|
13
48
|
function ReportModal() {
|
|
14
|
-
const { config, translations, isModalVisible, closeModal } = (0, useBugReporter_1.useBugReporter)();
|
|
49
|
+
const { config, translations, isModalVisible, closeModal, pendingScreenshot } = (0, useBugReporter_1.useBugReporter)();
|
|
15
50
|
const { captureAndUpload } = (0, useScreenCapture_1.useScreenCapture)();
|
|
16
51
|
const { getDeviceInfo, getAppInfo, getNetworkInfo } = (0, useDeviceInfo_1.useDeviceInfo)();
|
|
17
52
|
const [description, setDescription] = (0, react_1.useState)('');
|
|
@@ -21,15 +56,21 @@ function ReportModal() {
|
|
|
21
56
|
const [screenshotUrl, setScreenshotUrl] = (0, react_1.useState)(null);
|
|
22
57
|
const [isSubmitting, setIsSubmitting] = (0, react_1.useState)(false);
|
|
23
58
|
const categories = config.categories ?? constants_1.DEFAULT_CATEGORIES;
|
|
24
|
-
//
|
|
59
|
+
// Use pre-captured screenshot if available (shake flow), otherwise capture on open (button flow)
|
|
25
60
|
(0, react_1.useEffect)(() => {
|
|
26
61
|
if (isModalVisible) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
62
|
+
if (pendingScreenshot) {
|
|
63
|
+
setScreenshotUri(pendingScreenshot.uri);
|
|
64
|
+
setScreenshotUrl(pendingScreenshot.url || null);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
captureAndUpload().then((result) => {
|
|
68
|
+
if (result) {
|
|
69
|
+
setScreenshotUri(result.uri);
|
|
70
|
+
setScreenshotUrl(result.url || null);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
33
74
|
}
|
|
34
75
|
else {
|
|
35
76
|
// Reset form when modal closes
|
|
@@ -41,6 +82,36 @@ function ReportModal() {
|
|
|
41
82
|
setIsSubmitting(false);
|
|
42
83
|
}
|
|
43
84
|
}, [isModalVisible]);
|
|
85
|
+
const handlePickImage = (0, react_1.useCallback)(async () => {
|
|
86
|
+
const result = await ImagePicker.launchImageLibraryAsync({
|
|
87
|
+
mediaTypes: ['images'],
|
|
88
|
+
quality: 0.7,
|
|
89
|
+
base64: true,
|
|
90
|
+
});
|
|
91
|
+
if (result.canceled || !result.assets[0])
|
|
92
|
+
return;
|
|
93
|
+
const asset = result.assets[0];
|
|
94
|
+
setScreenshotUri(asset.uri);
|
|
95
|
+
// Upload to Supabase Storage if base64 is available
|
|
96
|
+
if (asset.base64) {
|
|
97
|
+
try {
|
|
98
|
+
const supabase = (0, supabase_js_1.createClient)(config.supabaseUrl, config.supabaseAnonKey);
|
|
99
|
+
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
100
|
+
const fileName = `${config.projectId}/${uniqueId}.jpg`;
|
|
101
|
+
const arrayBuffer = (0, base64_1.base64ToArrayBuffer)(asset.base64);
|
|
102
|
+
const { error } = await supabase.storage
|
|
103
|
+
.from('screenshots')
|
|
104
|
+
.upload(fileName, arrayBuffer, { contentType: 'image/jpeg' });
|
|
105
|
+
if (!error) {
|
|
106
|
+
const { data } = supabase.storage.from('screenshots').getPublicUrl(fileName);
|
|
107
|
+
setScreenshotUrl(data.publicUrl);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Upload failed, URI still available for preview
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}, [config]);
|
|
44
115
|
const handleSubmit = (0, react_1.useCallback)(async () => {
|
|
45
116
|
if (!description.trim())
|
|
46
117
|
return;
|
|
@@ -84,7 +155,7 @@ function ReportModal() {
|
|
|
84
155
|
setIsSubmitting(false);
|
|
85
156
|
}
|
|
86
157
|
}, [description, category, severity, screenshotUrl, config, translations, closeModal]);
|
|
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: [
|
|
158
|
+
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: [(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 }), screenshotUri ? ((0, jsx_runtime_1.jsx)(ScreenshotPreview_1.ScreenshotPreview, { uri: screenshotUri, onRemove: () => { setScreenshotUri(null); setScreenshotUrl(null); }, removeLabel: translations.removeScreenshot })) : null, (0, jsx_runtime_1.jsx)(react_native_1.TouchableOpacity, { style: styles.addImageButton, onPress: handlePickImage, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.addImageText, children: translations.addImage }) })] }), (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) => {
|
|
88
159
|
const labels = {
|
|
89
160
|
low: translations.severityLow,
|
|
90
161
|
medium: translations.severityMedium,
|
|
@@ -183,4 +254,19 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
183
254
|
chipTextActive: {
|
|
184
255
|
color: '#fff',
|
|
185
256
|
},
|
|
257
|
+
addImageButton: {
|
|
258
|
+
marginTop: 8,
|
|
259
|
+
paddingVertical: 10,
|
|
260
|
+
paddingHorizontal: 16,
|
|
261
|
+
borderRadius: 8,
|
|
262
|
+
borderWidth: 1,
|
|
263
|
+
borderColor: '#6366f1',
|
|
264
|
+
borderStyle: 'dashed',
|
|
265
|
+
alignItems: 'center',
|
|
266
|
+
},
|
|
267
|
+
addImageText: {
|
|
268
|
+
fontSize: 14,
|
|
269
|
+
color: '#6366f1',
|
|
270
|
+
fontWeight: '500',
|
|
271
|
+
},
|
|
186
272
|
});
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { RefObject } from 'react';
|
|
2
2
|
import type { View } from 'react-native';
|
|
3
3
|
import type { BugReporterConfig, Translations } from '../types';
|
|
4
|
+
export interface ScreenshotData {
|
|
5
|
+
uri: string;
|
|
6
|
+
url: string;
|
|
7
|
+
}
|
|
4
8
|
export interface BugReporterContextValue {
|
|
5
9
|
config: BugReporterConfig;
|
|
6
10
|
translations: Translations;
|
|
@@ -11,5 +15,6 @@ export interface BugReporterContextValue {
|
|
|
11
15
|
openBoard: () => void;
|
|
12
16
|
closeBoard: () => void;
|
|
13
17
|
viewRef: RefObject<View>;
|
|
18
|
+
pendingScreenshot: ScreenshotData | null;
|
|
14
19
|
}
|
|
15
20
|
export declare const BugReporterContext: import("react").Context<BugReporterContextValue | null>;
|
|
@@ -4,28 +4,71 @@ exports.BugReporterProvider = BugReporterProvider;
|
|
|
4
4
|
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
5
|
const react_1 = require("react");
|
|
6
6
|
const react_native_1 = require("react-native");
|
|
7
|
+
const react_native_view_shot_1 = require("react-native-view-shot");
|
|
8
|
+
const supabase_js_1 = require("@supabase/supabase-js");
|
|
7
9
|
const BugReporterContext_1 = require("./BugReporterContext");
|
|
8
10
|
const ReportModal_1 = require("../components/ReportModal");
|
|
9
11
|
const FeatureBoardModal_1 = require("../components/FeatureBoardModal");
|
|
10
12
|
const FloatingButton_1 = require("../components/FloatingButton");
|
|
11
13
|
const useShakeDetection_1 = require("../hooks/useShakeDetection");
|
|
12
14
|
const i18n_1 = require("../i18n");
|
|
15
|
+
const constants_1 = require("../constants");
|
|
16
|
+
const base64_1 = require("../utils/base64");
|
|
13
17
|
function BugReporterProvider({ config, children }) {
|
|
14
18
|
const [isModalVisible, setIsModalVisible] = (0, react_1.useState)(false);
|
|
15
19
|
const [isBoardVisible, setIsBoardVisible] = (0, react_1.useState)(false);
|
|
20
|
+
const [pendingScreenshot, setPendingScreenshot] = (0, react_1.useState)(null);
|
|
16
21
|
const viewRef = (0, react_1.useRef)(null);
|
|
17
22
|
const translations = (0, i18n_1.getTranslations)(config.locale);
|
|
18
23
|
const openModal = (0, react_1.useCallback)(() => setIsModalVisible(true), []);
|
|
19
|
-
const closeModal = (0, react_1.useCallback)(() =>
|
|
24
|
+
const closeModal = (0, react_1.useCallback)(() => {
|
|
25
|
+
setIsModalVisible(false);
|
|
26
|
+
setPendingScreenshot(null);
|
|
27
|
+
}, []);
|
|
20
28
|
const openBoard = (0, react_1.useCallback)(() => setIsBoardVisible(true), []);
|
|
21
29
|
const closeBoard = (0, react_1.useCallback)(() => setIsBoardVisible(false), []);
|
|
30
|
+
// Capture screenshot BEFORE opening modal to avoid iOS crash.
|
|
31
|
+
// Uses captureScreen to capture actual displayed pixels instead of view hierarchy,
|
|
32
|
+
// which avoids capturing the wrong screen in stack navigators.
|
|
33
|
+
const captureAndOpenModal = (0, react_1.useCallback)(async () => {
|
|
34
|
+
try {
|
|
35
|
+
const uri = await (0, react_native_view_shot_1.captureScreen)({
|
|
36
|
+
format: constants_1.SCREENSHOT_FORMAT,
|
|
37
|
+
quality: constants_1.SCREENSHOT_QUALITY,
|
|
38
|
+
});
|
|
39
|
+
const base64 = await (0, react_native_view_shot_1.captureScreen)({
|
|
40
|
+
format: constants_1.SCREENSHOT_FORMAT,
|
|
41
|
+
quality: constants_1.SCREENSHOT_QUALITY,
|
|
42
|
+
result: 'base64',
|
|
43
|
+
});
|
|
44
|
+
// Upload to Supabase Storage
|
|
45
|
+
const supabase = (0, supabase_js_1.createClient)(config.supabaseUrl, config.supabaseAnonKey);
|
|
46
|
+
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
47
|
+
const fileName = `${config.projectId}/${uniqueId}.jpg`;
|
|
48
|
+
const arrayBuffer = (0, base64_1.base64ToArrayBuffer)(base64);
|
|
49
|
+
const { error } = await supabase.storage
|
|
50
|
+
.from('screenshots')
|
|
51
|
+
.upload(fileName, arrayBuffer, { contentType: 'image/jpeg' });
|
|
52
|
+
if (error) {
|
|
53
|
+
setPendingScreenshot({ uri, url: '' });
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
const { data } = supabase.storage.from('screenshots').getPublicUrl(fileName);
|
|
57
|
+
setPendingScreenshot({ uri, url: data.publicUrl });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// Screenshot failed — open modal without screenshot
|
|
62
|
+
}
|
|
63
|
+
setIsModalVisible(true);
|
|
64
|
+
}, [config]);
|
|
22
65
|
const shakeEnabled = config.enableShake ?? !__DEV__;
|
|
23
66
|
(0, useShakeDetection_1.useShakeDetection)({
|
|
24
67
|
enabled: shakeEnabled,
|
|
25
68
|
threshold: config.shakeThreshold,
|
|
26
|
-
onShake:
|
|
69
|
+
onShake: captureAndOpenModal,
|
|
27
70
|
});
|
|
28
|
-
return ((0, jsx_runtime_1.jsxs)(BugReporterContext_1.BugReporterContext.Provider, { value: { config, translations, isModalVisible, openModal, closeModal, isBoardVisible, openBoard, closeBoard, viewRef }, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { ref: viewRef, collapsable: false, style: styles.container, children: children }), config.floatingButton !== false && ((0, jsx_runtime_1.jsx)(react_native_1.View, { pointerEvents: "box-none", style: react_native_1.StyleSheet.absoluteFill, children: (0, jsx_runtime_1.jsx)(FloatingButton_1.FloatingButton, { onPress:
|
|
71
|
+
return ((0, jsx_runtime_1.jsxs)(BugReporterContext_1.BugReporterContext.Provider, { value: { config, translations, isModalVisible, openModal, closeModal, isBoardVisible, openBoard, closeBoard, viewRef, pendingScreenshot }, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { ref: viewRef, collapsable: false, style: styles.container, children: children }), config.floatingButton !== false && ((0, jsx_runtime_1.jsx)(react_native_1.View, { pointerEvents: "box-none", style: react_native_1.StyleSheet.absoluteFill, children: (0, jsx_runtime_1.jsx)(FloatingButton_1.FloatingButton, { onPress: captureAndOpenModal, style: config.floatingButtonStyle }) })), (0, jsx_runtime_1.jsx)(ReportModal_1.ReportModal, {}), (0, jsx_runtime_1.jsx)(FeatureBoardModal_1.FeatureBoardModal, {})] }));
|
|
29
72
|
}
|
|
30
73
|
const styles = react_native_1.StyleSheet.create({
|
|
31
74
|
container: { flex: 1 },
|
|
@@ -6,50 +6,19 @@ 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
|
-
|
|
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
|
+
const base64_1 = require("../utils/base64");
|
|
41
10
|
function useScreenCapture() {
|
|
42
|
-
const {
|
|
11
|
+
const { config } = (0, useBugReporter_1.useBugReporter)();
|
|
43
12
|
const captureAndUpload = (0, react_1.useCallback)(async () => {
|
|
44
13
|
try {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
//
|
|
48
|
-
const uri = await (0, react_native_view_shot_1.
|
|
14
|
+
// Use captureScreen to capture the actual displayed screen pixels
|
|
15
|
+
// instead of captureRef which captures the view hierarchy and can
|
|
16
|
+
// show the wrong screen in stack navigators
|
|
17
|
+
const uri = await (0, react_native_view_shot_1.captureScreen)({
|
|
49
18
|
format: constants_1.SCREENSHOT_FORMAT,
|
|
50
19
|
quality: constants_1.SCREENSHOT_QUALITY,
|
|
51
20
|
});
|
|
52
|
-
const base64 = await (0, react_native_view_shot_1.
|
|
21
|
+
const base64 = await (0, react_native_view_shot_1.captureScreen)({
|
|
53
22
|
format: constants_1.SCREENSHOT_FORMAT,
|
|
54
23
|
quality: constants_1.SCREENSHOT_QUALITY,
|
|
55
24
|
result: 'base64',
|
|
@@ -58,7 +27,7 @@ function useScreenCapture() {
|
|
|
58
27
|
const supabase = (0, supabase_js_1.createClient)(config.supabaseUrl, config.supabaseAnonKey);
|
|
59
28
|
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
60
29
|
const fileName = `${config.projectId}/${uniqueId}.jpg`;
|
|
61
|
-
const arrayBuffer = base64ToArrayBuffer(base64);
|
|
30
|
+
const arrayBuffer = (0, base64_1.base64ToArrayBuffer)(base64);
|
|
62
31
|
const { error } = await supabase.storage
|
|
63
32
|
.from('screenshots')
|
|
64
33
|
.upload(fileName, arrayBuffer, { contentType: 'image/jpeg' });
|
|
@@ -72,6 +41,6 @@ function useScreenCapture() {
|
|
|
72
41
|
catch {
|
|
73
42
|
return null;
|
|
74
43
|
}
|
|
75
|
-
}, [
|
|
44
|
+
}, [config]);
|
|
76
45
|
return { captureAndUpload };
|
|
77
46
|
}
|
package/dist/i18n/en.js
CHANGED
package/dist/i18n/fr.js
CHANGED
package/dist/types.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { ViewStyle } from 'react-native';
|
|
|
2
2
|
type ViewStyleProp = ViewStyle;
|
|
3
3
|
export type BugCategory = 'Bug' | 'Crash' | 'UI' | 'Performance' | 'Feature Request' | 'Other';
|
|
4
4
|
export type BugSeverity = 'low' | 'medium' | 'high' | 'critical';
|
|
5
|
-
export type BugStatus = 'open' | 'in_progress' | 'resolved' | 'closed';
|
|
5
|
+
export type BugStatus = 'open' | 'in_progress' | 'resolved' | 'test' | 'closed';
|
|
6
6
|
export type SupportedLocale = 'en' | 'fr';
|
|
7
7
|
export type FeatureCategory = 'UI/UX' | 'Performance' | 'New Feature' | 'Integration' | 'Improvement' | 'Other';
|
|
8
8
|
export type FeatureStatus = 'under_review' | 'planned' | 'in_progress' | 'completed' | 'declined';
|
|
@@ -79,6 +79,7 @@ export interface Translations {
|
|
|
79
79
|
severityMedium: string;
|
|
80
80
|
severityHigh: string;
|
|
81
81
|
severityCritical: string;
|
|
82
|
+
addImage: string;
|
|
82
83
|
featureBoard: string;
|
|
83
84
|
suggestFeature: string;
|
|
84
85
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function base64ToArrayBuffer(base64: string): ArrayBuffer;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.base64ToArrayBuffer = base64ToArrayBuffer;
|
|
4
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
5
|
+
function base64ToArrayBuffer(base64) {
|
|
6
|
+
// Count padding from the original string before stripping
|
|
7
|
+
let padding = 0;
|
|
8
|
+
if (base64.endsWith('=='))
|
|
9
|
+
padding = 2;
|
|
10
|
+
else if (base64.endsWith('='))
|
|
11
|
+
padding = 1;
|
|
12
|
+
// Remove all non-base64 characters (including '=' padding)
|
|
13
|
+
const clean = base64.replace(/[^A-Za-z0-9+/]/g, '');
|
|
14
|
+
const len = clean.length;
|
|
15
|
+
// The byte length formula requires the padded base64 length (multiple of 4).
|
|
16
|
+
// Since clean has '=' stripped, we add padding back for the calculation.
|
|
17
|
+
const paddedLen = len + padding;
|
|
18
|
+
const byteLen = (paddedLen * 3) / 4 - padding;
|
|
19
|
+
const buffer = new ArrayBuffer(byteLen);
|
|
20
|
+
const bytes = new Uint8Array(buffer);
|
|
21
|
+
let p = 0;
|
|
22
|
+
for (let i = 0; i < len; i += 4) {
|
|
23
|
+
const a = chars.indexOf(clean[i]);
|
|
24
|
+
const b = chars.indexOf(clean[i + 1]);
|
|
25
|
+
const c = chars.indexOf(clean[i + 2]);
|
|
26
|
+
const d = chars.indexOf(clean[i + 3]);
|
|
27
|
+
const bits = (a << 18) | (b << 12) | (c << 6) | d;
|
|
28
|
+
bytes[p++] = (bits >> 16) & 0xff;
|
|
29
|
+
if (p < byteLen)
|
|
30
|
+
bytes[p++] = (bits >> 8) & 0xff;
|
|
31
|
+
if (p < byteLen)
|
|
32
|
+
bytes[p++] = bits & 0xff;
|
|
33
|
+
}
|
|
34
|
+
return buffer;
|
|
35
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mindedsolutions/bug-reporter-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
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",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"peerDependencies": {
|
|
24
24
|
"expo-application": ">=5 <8",
|
|
25
25
|
"expo-device": ">=6 <9",
|
|
26
|
+
"expo-image-picker": ">=15 <17",
|
|
26
27
|
"expo-network": ">=6 <9",
|
|
27
28
|
"expo-sensors": ">=13 <17",
|
|
28
29
|
"react": ">=18",
|
|
@@ -36,6 +37,7 @@
|
|
|
36
37
|
"devDependencies": {
|
|
37
38
|
"@types/react": "^18",
|
|
38
39
|
"@types/react-native": "^0.72",
|
|
40
|
+
"expo-image-picker": "^55.0.14",
|
|
39
41
|
"typescript": "^5.5.0"
|
|
40
42
|
}
|
|
41
43
|
}
|