@mindedsolutions/bug-reporter-sdk 0.3.9 → 0.3.10
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.
|
@@ -11,7 +11,7 @@ const useDeviceInfo_1 = require("../hooks/useDeviceInfo");
|
|
|
11
11
|
const ScreenshotPreview_1 = require("./ScreenshotPreview");
|
|
12
12
|
const constants_1 = require("../constants");
|
|
13
13
|
function ReportModal() {
|
|
14
|
-
const { config, translations, isModalVisible, closeModal } = (0, useBugReporter_1.useBugReporter)();
|
|
14
|
+
const { config, translations, isModalVisible, closeModal, pendingScreenshot } = (0, useBugReporter_1.useBugReporter)();
|
|
15
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)('');
|
|
@@ -21,15 +21,21 @@ function ReportModal() {
|
|
|
21
21
|
const [screenshotUrl, setScreenshotUrl] = (0, react_1.useState)(null);
|
|
22
22
|
const [isSubmitting, setIsSubmitting] = (0, react_1.useState)(false);
|
|
23
23
|
const categories = config.categories ?? constants_1.DEFAULT_CATEGORIES;
|
|
24
|
-
//
|
|
24
|
+
// Use pre-captured screenshot if available (shake flow), otherwise capture on open (button flow)
|
|
25
25
|
(0, react_1.useEffect)(() => {
|
|
26
26
|
if (isModalVisible) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
if (pendingScreenshot) {
|
|
28
|
+
setScreenshotUri(pendingScreenshot.uri);
|
|
29
|
+
setScreenshotUrl(pendingScreenshot.url || null);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
captureAndUpload().then((result) => {
|
|
33
|
+
if (result) {
|
|
34
|
+
setScreenshotUri(result.uri);
|
|
35
|
+
setScreenshotUrl(result.url || null);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
33
39
|
}
|
|
34
40
|
else {
|
|
35
41
|
// Reset form when modal closes
|
|
@@ -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,73 @@ 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
|
+
// drawViewHierarchyInRect:afterScreenUpdates: crashes when the view hierarchy
|
|
32
|
+
// is in transition (modal animating). Capturing first keeps the hierarchy stable.
|
|
33
|
+
const captureAndOpenModal = (0, react_1.useCallback)(async () => {
|
|
34
|
+
try {
|
|
35
|
+
if (viewRef.current) {
|
|
36
|
+
const uri = await (0, react_native_view_shot_1.captureRef)(viewRef.current, {
|
|
37
|
+
format: constants_1.SCREENSHOT_FORMAT,
|
|
38
|
+
quality: constants_1.SCREENSHOT_QUALITY,
|
|
39
|
+
});
|
|
40
|
+
const base64 = await (0, react_native_view_shot_1.captureRef)(viewRef.current, {
|
|
41
|
+
format: constants_1.SCREENSHOT_FORMAT,
|
|
42
|
+
quality: constants_1.SCREENSHOT_QUALITY,
|
|
43
|
+
result: 'base64',
|
|
44
|
+
});
|
|
45
|
+
// Upload to Supabase Storage
|
|
46
|
+
const supabase = (0, supabase_js_1.createClient)(config.supabaseUrl, config.supabaseAnonKey);
|
|
47
|
+
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
48
|
+
const fileName = `${config.projectId}/${uniqueId}.jpg`;
|
|
49
|
+
const arrayBuffer = (0, base64_1.base64ToArrayBuffer)(base64);
|
|
50
|
+
const { error } = await supabase.storage
|
|
51
|
+
.from('screenshots')
|
|
52
|
+
.upload(fileName, arrayBuffer, { contentType: 'image/jpeg' });
|
|
53
|
+
if (error) {
|
|
54
|
+
setPendingScreenshot({ uri, url: '' });
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
const { data } = supabase.storage.from('screenshots').getPublicUrl(fileName);
|
|
58
|
+
setPendingScreenshot({ uri, url: data.publicUrl });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// Screenshot failed — open modal without screenshot
|
|
64
|
+
}
|
|
65
|
+
setIsModalVisible(true);
|
|
66
|
+
}, [config]);
|
|
22
67
|
const shakeEnabled = config.enableShake ?? !__DEV__;
|
|
23
68
|
(0, useShakeDetection_1.useShakeDetection)({
|
|
24
69
|
enabled: shakeEnabled,
|
|
25
70
|
threshold: config.shakeThreshold,
|
|
26
|
-
onShake:
|
|
71
|
+
onShake: captureAndOpenModal,
|
|
27
72
|
});
|
|
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:
|
|
73
|
+
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
74
|
}
|
|
30
75
|
const styles = react_native_1.StyleSheet.create({
|
|
31
76
|
container: { flex: 1 },
|
|
@@ -6,38 +6,7 @@ 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
11
|
const { viewRef, config } = (0, useBugReporter_1.useBugReporter)();
|
|
43
12
|
const captureAndUpload = (0, react_1.useCallback)(async () => {
|
|
@@ -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' });
|
|
@@ -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.
|
|
3
|
+
"version": "0.3.10",
|
|
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",
|