@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.
@@ -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 { captureScreenshot, uploadScreenshot } = (0, useScreenCapture_1.useScreenCapture)();
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
- captureScreenshot().then(setScreenshotUri);
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, screenshotUri, config, translations, closeModal]);
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,4 @@
1
+ export declare function useFeatureBoard(): {
2
+ boardUrl: string | null;
3
+ openFeatureBoard: () => void;
4
+ };
@@ -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
+ }
@@ -1,4 +1,6 @@
1
1
  export declare function useScreenCapture(): {
2
- captureScreenshot: () => Promise<string | null>;
3
- uploadScreenshot: (uri: string) => Promise<string | null>;
2
+ captureAndUpload: () => Promise<{
3
+ uri: string;
4
+ url: string;
5
+ } | null>;
4
6
  };
@@ -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 captureScreenshot = (0, react_1.useCallback)(async () => {
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
- return uri;
20
- }
21
- catch {
22
- return null;
23
- }
24
- }, [viewRef, config]);
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
- // React Native: use FormData with file URI (fetch blob doesn't work reliably)
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, formData, { contentType: 'multipart/form-data' });
40
- if (error)
41
- return null;
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 { captureScreenshot, uploadScreenshot };
75
+ }, [viewRef, config]);
76
+ return { captureAndUpload };
50
77
  }
package/dist/i18n/en.js CHANGED
@@ -19,4 +19,6 @@ exports.en = {
19
19
  severityMedium: 'Medium',
20
20
  severityHigh: 'High',
21
21
  severityCritical: 'Critical',
22
+ featureBoard: 'Feature Board',
23
+ suggestFeature: 'Suggest a Feature',
22
24
  };
package/dist/i18n/fr.js CHANGED
@@ -19,4 +19,6 @@ exports.fr = {
19
19
  severityMedium: 'Moyen',
20
20
  severityHigh: 'Élevé',
21
21
  severityCritical: 'Critique',
22
+ featureBoard: 'Tableau des fonctionnalités',
23
+ suggestFeature: 'Suggérer une fonctionnalité',
22
24
  };
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 type { BugReporterConfig, BugReportPayload, BugCategory, BugSeverity, BugStatus, SupportedLocale, DeviceInfo, AppInfo, NetworkInfo, Translations, } from './types';
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.2",
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",