@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.
@@ -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
- // Capture screenshot and upload when modal opens
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
- captureAndUpload().then((result) => {
28
- if (result) {
29
- setScreenshotUri(result.uri);
30
- setScreenshotUrl(result.url || null);
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: [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) => {
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)(() => setIsModalVisible(false), []);
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: openModal,
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: openModal, style: config.floatingButtonStyle }) })), (0, jsx_runtime_1.jsx)(ReportModal_1.ReportModal, {}), (0, jsx_runtime_1.jsx)(FeatureBoardModal_1.FeatureBoardModal, {})] }));
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
- 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
+ const base64_1 = require("../utils/base64");
41
10
  function useScreenCapture() {
42
- const { viewRef, config } = (0, useBugReporter_1.useBugReporter)();
11
+ const { config } = (0, useBugReporter_1.useBugReporter)();
43
12
  const captureAndUpload = (0, react_1.useCallback)(async () => {
44
13
  try {
45
- if (!viewRef.current)
46
- return null;
47
- // Capture as both URI (for preview) and base64 (for upload)
48
- const uri = await (0, react_native_view_shot_1.captureRef)(viewRef.current, {
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.captureRef)(viewRef.current, {
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
- }, [viewRef, config]);
44
+ }, [config]);
76
45
  return { captureAndUpload };
77
46
  }
package/dist/i18n/en.js CHANGED
@@ -19,6 +19,7 @@ exports.en = {
19
19
  severityMedium: 'Medium',
20
20
  severityHigh: 'High',
21
21
  severityCritical: 'Critical',
22
+ addImage: 'Add Image',
22
23
  featureBoard: 'Feature Board',
23
24
  suggestFeature: 'Suggest a Feature',
24
25
  };
package/dist/i18n/fr.js CHANGED
@@ -19,6 +19,7 @@ exports.fr = {
19
19
  severityMedium: 'Moyen',
20
20
  severityHigh: 'Élevé',
21
21
  severityCritical: 'Critique',
22
+ addImage: 'Ajouter une image',
22
23
  featureBoard: 'Tableau des fonctionnalités',
23
24
  suggestFeature: 'Suggérer une fonctionnalité',
24
25
  };
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.9",
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
  }