react-native-bug-reporter 1.0.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.
Files changed (152) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +217 -0
  3. package/android/build.gradle +49 -0
  4. package/android/src/main/AndroidManifest.xml +11 -0
  5. package/android/src/main/java/com/bugreporter/ScreenshotDetectorModule.kt +227 -0
  6. package/android/src/main/java/com/bugreporter/ScreenshotDetectorPackage.kt +20 -0
  7. package/ios/RNBugReporterScreenshot.m +11 -0
  8. package/ios/RNBugReporterScreenshot.swift +113 -0
  9. package/lib/commonjs/BugReporterProvider.js +139 -0
  10. package/lib/commonjs/BugReporterProvider.js.map +1 -0
  11. package/lib/commonjs/collectors/appInfo.js +21 -0
  12. package/lib/commonjs/collectors/appInfo.js.map +1 -0
  13. package/lib/commonjs/collectors/collectContext.js +45 -0
  14. package/lib/commonjs/collectors/collectContext.js.map +1 -0
  15. package/lib/commonjs/collectors/deviceInfo.js +41 -0
  16. package/lib/commonjs/collectors/deviceInfo.js.map +1 -0
  17. package/lib/commonjs/collectors/networkInfo.js +33 -0
  18. package/lib/commonjs/collectors/networkInfo.js.map +1 -0
  19. package/lib/commonjs/components/BugReportAdminScreen.js +225 -0
  20. package/lib/commonjs/components/BugReportAdminScreen.js.map +1 -0
  21. package/lib/commonjs/components/BugReportModal.js +341 -0
  22. package/lib/commonjs/components/BugReportModal.js.map +1 -0
  23. package/lib/commonjs/components/ScreenshotEditor.js +466 -0
  24. package/lib/commonjs/components/ScreenshotEditor.js.map +1 -0
  25. package/lib/commonjs/components/ScreenshotPreview.js +134 -0
  26. package/lib/commonjs/components/ScreenshotPreview.js.map +1 -0
  27. package/lib/commonjs/components/SeveritySelector.js +65 -0
  28. package/lib/commonjs/components/SeveritySelector.js.map +1 -0
  29. package/lib/commonjs/context/BugReporterContext.js +24 -0
  30. package/lib/commonjs/context/BugReporterContext.js.map +1 -0
  31. package/lib/commonjs/hooks/useScreenshotDetector.js +22 -0
  32. package/lib/commonjs/hooks/useScreenshotDetector.js.map +1 -0
  33. package/lib/commonjs/index.js +87 -0
  34. package/lib/commonjs/index.js.map +1 -0
  35. package/lib/commonjs/native/ScreenshotDetector.js +72 -0
  36. package/lib/commonjs/native/ScreenshotDetector.js.map +1 -0
  37. package/lib/commonjs/navigation/screenTracker.js +47 -0
  38. package/lib/commonjs/navigation/screenTracker.js.map +1 -0
  39. package/lib/commonjs/package.json +1 -0
  40. package/lib/commonjs/services/bugReportService.js +61 -0
  41. package/lib/commonjs/services/bugReportService.js.map +1 -0
  42. package/lib/commonjs/services/supabaseService.js +166 -0
  43. package/lib/commonjs/services/supabaseService.js.map +1 -0
  44. package/lib/commonjs/theme.js +28 -0
  45. package/lib/commonjs/theme.js.map +1 -0
  46. package/lib/commonjs/types.js +35 -0
  47. package/lib/commonjs/types.js.map +1 -0
  48. package/lib/commonjs/utils/logger.js +29 -0
  49. package/lib/commonjs/utils/logger.js.map +1 -0
  50. package/lib/module/BugReporterProvider.js +134 -0
  51. package/lib/module/BugReporterProvider.js.map +1 -0
  52. package/lib/module/collectors/appInfo.js +16 -0
  53. package/lib/module/collectors/appInfo.js.map +1 -0
  54. package/lib/module/collectors/collectContext.js +41 -0
  55. package/lib/module/collectors/collectContext.js.map +1 -0
  56. package/lib/module/collectors/deviceInfo.js +37 -0
  57. package/lib/module/collectors/deviceInfo.js.map +1 -0
  58. package/lib/module/collectors/networkInfo.js +29 -0
  59. package/lib/module/collectors/networkInfo.js.map +1 -0
  60. package/lib/module/components/BugReportAdminScreen.js +221 -0
  61. package/lib/module/components/BugReportAdminScreen.js.map +1 -0
  62. package/lib/module/components/BugReportModal.js +337 -0
  63. package/lib/module/components/BugReportModal.js.map +1 -0
  64. package/lib/module/components/ScreenshotEditor.js +461 -0
  65. package/lib/module/components/ScreenshotEditor.js.map +1 -0
  66. package/lib/module/components/ScreenshotPreview.js +130 -0
  67. package/lib/module/components/ScreenshotPreview.js.map +1 -0
  68. package/lib/module/components/SeveritySelector.js +61 -0
  69. package/lib/module/components/SeveritySelector.js.map +1 -0
  70. package/lib/module/context/BugReporterContext.js +19 -0
  71. package/lib/module/context/BugReporterContext.js.map +1 -0
  72. package/lib/module/hooks/useScreenshotDetector.js +18 -0
  73. package/lib/module/hooks/useScreenshotDetector.js.map +1 -0
  74. package/lib/module/index.js +32 -0
  75. package/lib/module/index.js.map +1 -0
  76. package/lib/module/native/ScreenshotDetector.js +68 -0
  77. package/lib/module/native/ScreenshotDetector.js.map +1 -0
  78. package/lib/module/navigation/screenTracker.js +41 -0
  79. package/lib/module/navigation/screenTracker.js.map +1 -0
  80. package/lib/module/services/bugReportService.js +57 -0
  81. package/lib/module/services/bugReportService.js.map +1 -0
  82. package/lib/module/services/supabaseService.js +159 -0
  83. package/lib/module/services/supabaseService.js.map +1 -0
  84. package/lib/module/theme.js +23 -0
  85. package/lib/module/theme.js.map +1 -0
  86. package/lib/module/types.js +31 -0
  87. package/lib/module/types.js.map +1 -0
  88. package/lib/module/utils/logger.js +25 -0
  89. package/lib/module/utils/logger.js.map +1 -0
  90. package/lib/typescript/src/BugReporterProvider.d.ts +18 -0
  91. package/lib/typescript/src/BugReporterProvider.d.ts.map +1 -0
  92. package/lib/typescript/src/collectors/appInfo.d.ts +6 -0
  93. package/lib/typescript/src/collectors/appInfo.d.ts.map +1 -0
  94. package/lib/typescript/src/collectors/collectContext.d.ts +7 -0
  95. package/lib/typescript/src/collectors/collectContext.d.ts.map +1 -0
  96. package/lib/typescript/src/collectors/deviceInfo.d.ts +7 -0
  97. package/lib/typescript/src/collectors/deviceInfo.d.ts.map +1 -0
  98. package/lib/typescript/src/collectors/networkInfo.d.ts +6 -0
  99. package/lib/typescript/src/collectors/networkInfo.d.ts.map +1 -0
  100. package/lib/typescript/src/components/BugReportAdminScreen.d.ts +11 -0
  101. package/lib/typescript/src/components/BugReportAdminScreen.d.ts.map +1 -0
  102. package/lib/typescript/src/components/BugReportModal.d.ts +20 -0
  103. package/lib/typescript/src/components/BugReportModal.d.ts.map +1 -0
  104. package/lib/typescript/src/components/ScreenshotEditor.d.ts +16 -0
  105. package/lib/typescript/src/components/ScreenshotEditor.d.ts.map +1 -0
  106. package/lib/typescript/src/components/ScreenshotPreview.d.ts +11 -0
  107. package/lib/typescript/src/components/ScreenshotPreview.d.ts.map +1 -0
  108. package/lib/typescript/src/components/SeveritySelector.d.ts +10 -0
  109. package/lib/typescript/src/components/SeveritySelector.d.ts.map +1 -0
  110. package/lib/typescript/src/context/BugReporterContext.d.ts +20 -0
  111. package/lib/typescript/src/context/BugReporterContext.d.ts.map +1 -0
  112. package/lib/typescript/src/hooks/useScreenshotDetector.d.ts +7 -0
  113. package/lib/typescript/src/hooks/useScreenshotDetector.d.ts.map +1 -0
  114. package/lib/typescript/src/index.d.ts +26 -0
  115. package/lib/typescript/src/index.d.ts.map +1 -0
  116. package/lib/typescript/src/native/ScreenshotDetector.d.ts +15 -0
  117. package/lib/typescript/src/native/ScreenshotDetector.d.ts.map +1 -0
  118. package/lib/typescript/src/navigation/screenTracker.d.ts +7 -0
  119. package/lib/typescript/src/navigation/screenTracker.d.ts.map +1 -0
  120. package/lib/typescript/src/services/bugReportService.d.ts +17 -0
  121. package/lib/typescript/src/services/bugReportService.d.ts.map +1 -0
  122. package/lib/typescript/src/services/supabaseService.d.ts +38 -0
  123. package/lib/typescript/src/services/supabaseService.d.ts.map +1 -0
  124. package/lib/typescript/src/theme.d.ts +7 -0
  125. package/lib/typescript/src/theme.d.ts.map +1 -0
  126. package/lib/typescript/src/types.d.ts +144 -0
  127. package/lib/typescript/src/types.d.ts.map +1 -0
  128. package/lib/typescript/src/utils/logger.d.ts +7 -0
  129. package/lib/typescript/src/utils/logger.d.ts.map +1 -0
  130. package/package.json +100 -0
  131. package/react-native-bug-reporter.podspec +22 -0
  132. package/react-native.config.js +18 -0
  133. package/src/BugReporterProvider.tsx +178 -0
  134. package/src/collectors/appInfo.ts +15 -0
  135. package/src/collectors/collectContext.ts +47 -0
  136. package/src/collectors/deviceInfo.ts +51 -0
  137. package/src/collectors/networkInfo.ts +31 -0
  138. package/src/components/BugReportAdminScreen.tsx +160 -0
  139. package/src/components/BugReportModal.tsx +315 -0
  140. package/src/components/ScreenshotEditor.tsx +410 -0
  141. package/src/components/ScreenshotPreview.tsx +98 -0
  142. package/src/components/SeveritySelector.tsx +59 -0
  143. package/src/context/BugReporterContext.ts +29 -0
  144. package/src/hooks/useScreenshotDetector.ts +20 -0
  145. package/src/index.ts +51 -0
  146. package/src/native/ScreenshotDetector.ts +87 -0
  147. package/src/navigation/screenTracker.ts +40 -0
  148. package/src/services/bugReportService.ts +81 -0
  149. package/src/services/supabaseService.ts +195 -0
  150. package/src/theme.ts +23 -0
  151. package/src/types.ts +156 -0
  152. package/src/utils/logger.ts +24 -0
@@ -0,0 +1,410 @@
1
+ import { Fragment, useMemo, useRef, useState } from 'react';
2
+ import {
3
+ ActivityIndicator,
4
+ Image,
5
+ Modal,
6
+ PanResponder,
7
+ Platform,
8
+ StatusBar,
9
+ StyleSheet,
10
+ Text,
11
+ TextInput,
12
+ TouchableOpacity,
13
+ View,
14
+ type GestureResponderEvent,
15
+ } from 'react-native';
16
+ import Svg, {
17
+ Circle,
18
+ Line,
19
+ Path,
20
+ Polygon,
21
+ Text as SvgText,
22
+ } from 'react-native-svg';
23
+ import { captureRef } from 'react-native-view-shot';
24
+ import type { ScreenshotData } from '../types';
25
+ import type { ResolvedTheme } from '../theme';
26
+ import { logger } from '../utils/logger';
27
+
28
+ type Tool = 'pen' | 'arrow' | 'point' | 'text';
29
+
30
+ interface PathEl {
31
+ kind: 'path';
32
+ d: string;
33
+ color: string;
34
+ width: number;
35
+ }
36
+ interface ArrowEl {
37
+ kind: 'arrow';
38
+ x1: number;
39
+ y1: number;
40
+ x2: number;
41
+ y2: number;
42
+ color: string;
43
+ width: number;
44
+ }
45
+ interface PointEl {
46
+ kind: 'point';
47
+ x: number;
48
+ y: number;
49
+ color: string;
50
+ }
51
+ interface TextEl {
52
+ kind: 'text';
53
+ x: number;
54
+ y: number;
55
+ text: string;
56
+ color: string;
57
+ }
58
+ type Element = PathEl | ArrowEl | PointEl | TextEl;
59
+
60
+ interface Props {
61
+ screenshot: ScreenshotData;
62
+ theme: ResolvedTheme;
63
+ onCancel: () => void;
64
+ onSave: (uri: string) => void;
65
+ }
66
+
67
+ const COLORS = ['#ef4444', '#f59e0b', '#22c55e', '#3b82f6', '#ffffff', '#111827'];
68
+ const STROKE = 4;
69
+ const ARROW_HEAD = 16;
70
+ const TEXT_SIZE = 22;
71
+
72
+ /**
73
+ * Full-screen screenshot annotation editor. Lets the reporter draw on the
74
+ * captured screenshot to point at the bug — freehand pen, arrows, points, and
75
+ * text labels — then flattens everything into a single PNG via view-shot.
76
+ */
77
+ export function ScreenshotEditor({ screenshot, theme, onCancel, onSave }: Props) {
78
+ const [tool, setTool] = useState<Tool>('pen');
79
+ const [color, setColor] = useState<string>(COLORS[0]);
80
+ const [elements, setElements] = useState<Element[]>([]);
81
+ const [draft, setDraft] = useState<Element | null>(null);
82
+ const [saving, setSaving] = useState(false);
83
+ const [textPrompt, setTextPrompt] = useState<{ x: number; y: number; value: string } | null>(null);
84
+
85
+ const canvasRef = useRef<View>(null);
86
+ // Refs so the (stable) PanResponder always sees the latest tool/color.
87
+ const toolRef = useRef(tool);
88
+ toolRef.current = tool;
89
+ const colorRef = useRef(color);
90
+ colorRef.current = color;
91
+ const draftRef = useRef<Element | null>(null);
92
+
93
+ const panResponder = useMemo(
94
+ () =>
95
+ PanResponder.create({
96
+ onStartShouldSetPanResponder: () => true,
97
+ onMoveShouldSetPanResponder: () => true,
98
+ onPanResponderGrant: (e: GestureResponderEvent) => {
99
+ const { locationX: x, locationY: y } = e.nativeEvent;
100
+ const t = toolRef.current;
101
+ const c = colorRef.current;
102
+ if (t === 'pen') {
103
+ draftRef.current = { kind: 'path', d: `M${x} ${y}`, color: c, width: STROKE };
104
+ setDraft(draftRef.current);
105
+ } else if (t === 'arrow') {
106
+ draftRef.current = { kind: 'arrow', x1: x, y1: y, x2: x, y2: y, color: c, width: STROKE };
107
+ setDraft(draftRef.current);
108
+ } else if (t === 'point') {
109
+ setElements(prev => [...prev, { kind: 'point', x, y, color: c }]);
110
+ } else if (t === 'text') {
111
+ setTextPrompt({ x, y, value: '' });
112
+ }
113
+ },
114
+ onPanResponderMove: (e: GestureResponderEvent) => {
115
+ const { locationX: x, locationY: y } = e.nativeEvent;
116
+ const d = draftRef.current;
117
+ if (!d) return;
118
+ if (d.kind === 'path') {
119
+ d.d += ` L${x} ${y}`;
120
+ setDraft({ ...d });
121
+ } else if (d.kind === 'arrow') {
122
+ d.x2 = x;
123
+ d.y2 = y;
124
+ setDraft({ ...d });
125
+ }
126
+ },
127
+ onPanResponderRelease: () => {
128
+ const d = draftRef.current;
129
+ if (d) {
130
+ setElements(prev => [...prev, d]);
131
+ draftRef.current = null;
132
+ setDraft(null);
133
+ }
134
+ },
135
+ }),
136
+ [],
137
+ );
138
+
139
+ const undo = () => setElements(prev => prev.slice(0, -1));
140
+ const clear = () => {
141
+ setElements([]);
142
+ setDraft(null);
143
+ draftRef.current = null;
144
+ };
145
+
146
+ const commitText = () => {
147
+ if (textPrompt && textPrompt.value.trim()) {
148
+ setElements(prev => [
149
+ ...prev,
150
+ { kind: 'text', x: textPrompt.x, y: textPrompt.y, text: textPrompt.value.trim(), color },
151
+ ]);
152
+ }
153
+ setTextPrompt(null);
154
+ };
155
+
156
+ const handleSave = async () => {
157
+ // Nothing drawn — just keep the original.
158
+ if (elements.length === 0) {
159
+ onSave(screenshot.uri);
160
+ return;
161
+ }
162
+ setSaving(true);
163
+ try {
164
+ // Let the SVG/layout settle before snapshotting (Android needs a tick).
165
+ await new Promise<void>(res => setTimeout(() => res(), 150));
166
+ let uri = await captureRef(canvasRef, {
167
+ format: 'png',
168
+ quality: 1,
169
+ result: 'tmpfile',
170
+ });
171
+ if (uri && !uri.startsWith('file://') && !uri.startsWith('content://')) {
172
+ uri = `file://${uri}`;
173
+ }
174
+ onSave(uri || screenshot.uri);
175
+ } catch (e) {
176
+ logger.warn('failed to flatten annotated screenshot', e);
177
+ onSave(screenshot.uri); // fall back to the un-annotated image
178
+ } finally {
179
+ setSaving(false);
180
+ }
181
+ };
182
+
183
+ const all = draft ? [...elements, draft] : elements;
184
+
185
+ return (
186
+ <Modal visible animationType="slide" onRequestClose={onCancel}>
187
+ <View style={[styles.container, { backgroundColor: '#000' }]}>
188
+ {/* Top bar */}
189
+ <View style={styles.topBar}>
190
+ <TouchableOpacity onPress={onCancel} style={styles.barBtn} disabled={saving}>
191
+ <Text style={styles.barText}>Cancel</Text>
192
+ </TouchableOpacity>
193
+ <Text style={styles.barTitle}>Mark the bug</Text>
194
+ <TouchableOpacity
195
+ onPress={handleSave}
196
+ disabled={saving}
197
+ style={[styles.saveBtn, { backgroundColor: theme.primaryColor }]}>
198
+ {saving ? (
199
+ <ActivityIndicator color="#fff" size="small" />
200
+ ) : (
201
+ <Text style={styles.saveText}>✓ Save</Text>
202
+ )}
203
+ </TouchableOpacity>
204
+ </View>
205
+
206
+ {/* Canvas — fills the space between top bar and tools so the toolbar
207
+ and Save button are always visible regardless of screenshot size. */}
208
+ <View style={styles.canvasWrap}>
209
+ <View ref={canvasRef} collapsable={false} style={styles.canvas}>
210
+ <Image source={{ uri: screenshot.uri }} style={StyleSheet.absoluteFill} resizeMode="contain" />
211
+ <Svg style={StyleSheet.absoluteFill} {...panResponder.panHandlers}>
212
+ {all.map((el, i) => renderEl(el, i))}
213
+ </Svg>
214
+ </View>
215
+ </View>
216
+
217
+ {/* Color row */}
218
+ <View style={styles.colorRow}>
219
+ {COLORS.map(c => (
220
+ <TouchableOpacity
221
+ key={c}
222
+ onPress={() => setColor(c)}
223
+ style={[
224
+ styles.swatch,
225
+ { backgroundColor: c },
226
+ color === c && styles.swatchActive,
227
+ ]}
228
+ />
229
+ ))}
230
+ </View>
231
+
232
+ {/* Tool row */}
233
+ <View style={styles.toolRow}>
234
+ <ToolButton label="✏️ Pen" active={tool === 'pen'} onPress={() => setTool('pen')} />
235
+ <ToolButton label="➴ Arrow" active={tool === 'arrow'} onPress={() => setTool('arrow')} />
236
+ <ToolButton label="● Point" active={tool === 'point'} onPress={() => setTool('point')} />
237
+ <ToolButton label="T Text" active={tool === 'text'} onPress={() => setTool('text')} />
238
+ <ToolButton label="↺ Undo" onPress={undo} />
239
+ <ToolButton label="🗑 Clear" onPress={clear} />
240
+ </View>
241
+
242
+ {/* Text entry */}
243
+ {textPrompt && (
244
+ <View style={styles.textPrompt}>
245
+ <TextInput
246
+ autoFocus
247
+ value={textPrompt.value}
248
+ onChangeText={v => setTextPrompt(p => (p ? { ...p, value: v } : p))}
249
+ onSubmitEditing={commitText}
250
+ placeholder="Type a label…"
251
+ placeholderTextColor="#888"
252
+ style={styles.textInput}
253
+ />
254
+ <TouchableOpacity onPress={commitText} style={styles.textAdd}>
255
+ <Text style={styles.textAddLabel}>Add</Text>
256
+ </TouchableOpacity>
257
+ </View>
258
+ )}
259
+ </View>
260
+ </Modal>
261
+ );
262
+ }
263
+
264
+ function renderEl(el: Element, key: number) {
265
+ switch (el.kind) {
266
+ case 'path':
267
+ return (
268
+ <Path
269
+ key={key}
270
+ d={el.d}
271
+ stroke={el.color}
272
+ strokeWidth={el.width}
273
+ fill="none"
274
+ strokeLinecap="round"
275
+ strokeLinejoin="round"
276
+ />
277
+ );
278
+ case 'arrow': {
279
+ const angle = Math.atan2(el.y2 - el.y1, el.x2 - el.x1);
280
+ const a1 = angle - Math.PI / 6;
281
+ const a2 = angle + Math.PI / 6;
282
+ const head = `${el.x2},${el.y2} ${el.x2 - ARROW_HEAD * Math.cos(a1)},${
283
+ el.y2 - ARROW_HEAD * Math.sin(a1)
284
+ } ${el.x2 - ARROW_HEAD * Math.cos(a2)},${el.y2 - ARROW_HEAD * Math.sin(a2)}`;
285
+ return (
286
+ <Fragment key={key}>
287
+ <Line x1={el.x1} y1={el.y1} x2={el.x2} y2={el.y2} stroke={el.color} strokeWidth={el.width} strokeLinecap="round" />
288
+ <Polygon points={head} fill={el.color} />
289
+ </Fragment>
290
+ );
291
+ }
292
+ case 'point':
293
+ return (
294
+ <Circle key={key} cx={el.x} cy={el.y} r={9} fill="none" stroke={el.color} strokeWidth={4} />
295
+ );
296
+ case 'text':
297
+ return (
298
+ <SvgText
299
+ key={key}
300
+ x={el.x}
301
+ y={el.y}
302
+ fill={el.color}
303
+ fontSize={TEXT_SIZE}
304
+ fontWeight="bold"
305
+ stroke="#00000055"
306
+ strokeWidth={0.5}>
307
+ {el.text}
308
+ </SvgText>
309
+ );
310
+ default:
311
+ return null;
312
+ }
313
+ }
314
+
315
+ function ToolButton({
316
+ label,
317
+ active,
318
+ onPress,
319
+ }: {
320
+ label: string;
321
+ active?: boolean;
322
+ onPress: () => void;
323
+ }) {
324
+ return (
325
+ <TouchableOpacity
326
+ onPress={onPress}
327
+ style={[styles.toolBtn, active && styles.toolBtnActive]}>
328
+ <Text style={[styles.toolLabel, active && styles.toolLabelActive]}>{label}</Text>
329
+ </TouchableOpacity>
330
+ );
331
+ }
332
+
333
+ const styles = StyleSheet.create({
334
+ container: { flex: 1 },
335
+ topBar: {
336
+ flexDirection: 'row',
337
+ alignItems: 'center',
338
+ justifyContent: 'space-between',
339
+ paddingHorizontal: 14,
340
+ paddingTop: (Platform.OS === 'android' ? StatusBar.currentHeight ?? 24 : 50) + 8,
341
+ paddingBottom: 10,
342
+ },
343
+ barBtn: { padding: 6, minWidth: 64 },
344
+ barText: { color: '#fff', fontSize: 16 },
345
+ barTitle: { color: '#fff', fontSize: 16, fontWeight: '600' },
346
+ saveBtn: {
347
+ minWidth: 84,
348
+ paddingVertical: 9,
349
+ paddingHorizontal: 16,
350
+ borderRadius: 10,
351
+ alignItems: 'center',
352
+ justifyContent: 'center',
353
+ },
354
+ saveText: { color: '#fff', fontSize: 15, fontWeight: '800' },
355
+ canvasWrap: { flex: 1, alignItems: 'center', justifyContent: 'center' },
356
+ canvas: { flex: 1, width: '100%', backgroundColor: '#000' },
357
+ colorRow: {
358
+ flexDirection: 'row',
359
+ justifyContent: 'center',
360
+ gap: 12,
361
+ paddingVertical: 12,
362
+ },
363
+ swatch: { width: 28, height: 28, borderRadius: 14, borderWidth: 2, borderColor: '#444' },
364
+ swatchActive: { borderColor: '#fff', transform: [{ scale: 1.15 }] },
365
+ toolRow: {
366
+ flexDirection: 'row',
367
+ flexWrap: 'wrap',
368
+ justifyContent: 'center',
369
+ gap: 8,
370
+ paddingHorizontal: 10,
371
+ paddingBottom: 34,
372
+ paddingTop: 4,
373
+ },
374
+ toolBtn: {
375
+ paddingVertical: 9,
376
+ paddingHorizontal: 12,
377
+ borderRadius: 10,
378
+ backgroundColor: '#1c1f26',
379
+ },
380
+ toolBtnActive: { backgroundColor: '#2563eb' },
381
+ toolLabel: { color: '#cbd2dc', fontSize: 13, fontWeight: '600' },
382
+ toolLabelActive: { color: '#fff' },
383
+ textPrompt: {
384
+ position: 'absolute',
385
+ bottom: 0,
386
+ left: 0,
387
+ right: 0,
388
+ flexDirection: 'row',
389
+ gap: 8,
390
+ padding: 12,
391
+ paddingBottom: 28,
392
+ backgroundColor: '#14161c',
393
+ },
394
+ textInput: {
395
+ flex: 1,
396
+ backgroundColor: '#0b0c10',
397
+ color: '#fff',
398
+ borderRadius: 8,
399
+ paddingHorizontal: 12,
400
+ paddingVertical: 10,
401
+ fontSize: 15,
402
+ },
403
+ textAdd: {
404
+ backgroundColor: '#2563eb',
405
+ borderRadius: 8,
406
+ paddingHorizontal: 18,
407
+ justifyContent: 'center',
408
+ },
409
+ textAddLabel: { color: '#fff', fontWeight: '700' },
410
+ });
@@ -0,0 +1,98 @@
1
+ import { Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
2
+ import type { ScreenshotData } from '../types';
3
+ import type { ResolvedTheme } from '../theme';
4
+
5
+ interface Props {
6
+ screenshot: ScreenshotData | null;
7
+ onRemove: () => void;
8
+ onEdit?: () => void;
9
+ theme: ResolvedTheme;
10
+ }
11
+
12
+ export function ScreenshotPreview({ screenshot, onRemove, onEdit, theme }: Props) {
13
+ if (!screenshot?.uri) {
14
+ return (
15
+ <View style={[styles.empty, { borderColor: theme.borderColor }]}>
16
+ <Text style={[styles.emptyText, { color: theme.mutedColor }]}>
17
+ No screenshot attached
18
+ </Text>
19
+ </View>
20
+ );
21
+ }
22
+
23
+ return (
24
+ <View style={[styles.container, { borderColor: theme.borderColor }]}>
25
+ <Image source={{ uri: screenshot.uri }} style={styles.image} resizeMode="contain" />
26
+ <TouchableOpacity
27
+ accessibilityRole="button"
28
+ accessibilityLabel="Remove screenshot"
29
+ onPress={onRemove}
30
+ style={styles.removeBtn}>
31
+ <Text style={styles.removeText}>✕</Text>
32
+ </TouchableOpacity>
33
+ {onEdit && (
34
+ <TouchableOpacity
35
+ accessibilityRole="button"
36
+ accessibilityLabel="Annotate screenshot"
37
+ onPress={onEdit}
38
+ style={[styles.editBtn, { backgroundColor: theme.primaryColor }]}>
39
+ <Text style={styles.editText}>✏️ Mark the bug</Text>
40
+ </TouchableOpacity>
41
+ )}
42
+ <View style={styles.badge}>
43
+ <Text style={styles.badgeText}>Attached</Text>
44
+ </View>
45
+ </View>
46
+ );
47
+ }
48
+
49
+ const styles = StyleSheet.create({
50
+ container: {
51
+ height: 180,
52
+ borderRadius: 12,
53
+ borderWidth: 1,
54
+ overflow: 'hidden',
55
+ backgroundColor: '#0b0c10',
56
+ },
57
+ image: { width: '100%', height: '100%' },
58
+ empty: {
59
+ height: 80,
60
+ borderRadius: 12,
61
+ borderWidth: 1,
62
+ borderStyle: 'dashed',
63
+ alignItems: 'center',
64
+ justifyContent: 'center',
65
+ },
66
+ emptyText: { fontSize: 13 },
67
+ removeBtn: {
68
+ position: 'absolute',
69
+ top: 8,
70
+ right: 8,
71
+ width: 28,
72
+ height: 28,
73
+ borderRadius: 14,
74
+ backgroundColor: 'rgba(0,0,0,0.6)',
75
+ alignItems: 'center',
76
+ justifyContent: 'center',
77
+ },
78
+ removeText: { color: '#fff', fontSize: 14, fontWeight: '700' },
79
+ editBtn: {
80
+ position: 'absolute',
81
+ bottom: 8,
82
+ right: 8,
83
+ paddingHorizontal: 12,
84
+ paddingVertical: 6,
85
+ borderRadius: 8,
86
+ },
87
+ editText: { color: '#fff', fontSize: 12, fontWeight: '700' },
88
+ badge: {
89
+ position: 'absolute',
90
+ bottom: 8,
91
+ left: 8,
92
+ paddingHorizontal: 8,
93
+ paddingVertical: 3,
94
+ borderRadius: 6,
95
+ backgroundColor: 'rgba(37,99,235,0.9)',
96
+ },
97
+ badgeText: { color: '#fff', fontSize: 11, fontWeight: '600' },
98
+ });
@@ -0,0 +1,59 @@
1
+ import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
2
+ import { SEVERITIES, type BugSeverity } from '../types';
3
+ import { SEVERITY_COLORS, type ResolvedTheme } from '../theme';
4
+
5
+ interface Props {
6
+ value: BugSeverity;
7
+ onChange: (value: BugSeverity) => void;
8
+ theme: ResolvedTheme;
9
+ }
10
+
11
+ const LABELS: Record<BugSeverity, string> = {
12
+ low: 'Low',
13
+ medium: 'Medium',
14
+ high: 'High',
15
+ critical: 'Critical',
16
+ };
17
+
18
+ export function SeveritySelector({ value, onChange, theme }: Props) {
19
+ return (
20
+ <View style={styles.row}>
21
+ {SEVERITIES.map(sev => {
22
+ const selected = sev === value;
23
+ const color = SEVERITY_COLORS[sev];
24
+ return (
25
+ <TouchableOpacity
26
+ key={sev}
27
+ accessibilityRole="button"
28
+ accessibilityState={{ selected }}
29
+ onPress={() => onChange(sev)}
30
+ style={[
31
+ styles.chip,
32
+ { borderColor: selected ? color : theme.borderColor },
33
+ selected && { backgroundColor: color },
34
+ ]}>
35
+ <Text
36
+ style={[
37
+ styles.label,
38
+ { color: selected ? '#fff' : theme.mutedColor },
39
+ ]}>
40
+ {LABELS[sev]}
41
+ </Text>
42
+ </TouchableOpacity>
43
+ );
44
+ })}
45
+ </View>
46
+ );
47
+ }
48
+
49
+ const styles = StyleSheet.create({
50
+ row: { flexDirection: 'row', gap: 8 },
51
+ chip: {
52
+ flex: 1,
53
+ paddingVertical: 10,
54
+ borderRadius: 10,
55
+ borderWidth: 1.5,
56
+ alignItems: 'center',
57
+ },
58
+ label: { fontSize: 13, fontWeight: '600' },
59
+ });
@@ -0,0 +1,29 @@
1
+ import { createContext, useContext } from 'react';
2
+ import type { ScreenshotData } from '../types';
3
+
4
+ export interface BugReporterContextValue {
5
+ /** Whether automatic screenshot detection is currently active. */
6
+ isAutoDetectEnabled: boolean;
7
+ /** Open the report modal manually, optionally pre-attaching a screenshot. */
8
+ openReporter: (screenshot?: ScreenshotData | null) => void;
9
+ /** Close the report modal. */
10
+ closeReporter: () => void;
11
+ /** True while the modal is visible. */
12
+ isOpen: boolean;
13
+ }
14
+
15
+ export const BugReporterContext = createContext<BugReporterContextValue | null>(null);
16
+
17
+ /**
18
+ * Access the bug reporter imperatively, e.g. to wire a "Report a bug" button:
19
+ *
20
+ * const { openReporter } = useBugReporter();
21
+ * <Button title="Report a bug" onPress={() => openReporter()} />
22
+ */
23
+ export function useBugReporter(): BugReporterContextValue {
24
+ const ctx = useContext(BugReporterContext);
25
+ if (!ctx) {
26
+ throw new Error('useBugReporter must be used within a <BugReporterProvider>');
27
+ }
28
+ return ctx;
29
+ }
@@ -0,0 +1,20 @@
1
+ import { useEffect } from 'react';
2
+ import { screenshotDetector } from '../native/ScreenshotDetector';
3
+ import type { ScreenshotData } from '../types';
4
+
5
+ /**
6
+ * Subscribes to native screenshot detection while `enabled` is true.
7
+ * Calls `onScreenshot` with the captured image whenever the user takes one.
8
+ */
9
+ export function useScreenshotDetector(
10
+ enabled: boolean,
11
+ onScreenshot: (screenshot: ScreenshotData) => void,
12
+ ) {
13
+ useEffect(() => {
14
+ if (!enabled) {
15
+ return;
16
+ }
17
+ const unsubscribe = screenshotDetector.subscribe(onScreenshot);
18
+ return unsubscribe;
19
+ }, [enabled, onScreenshot]);
20
+ }
package/src/index.ts ADDED
@@ -0,0 +1,51 @@
1
+ /**
2
+ * React Native Universal Bug Reporter SDK
3
+ * ---------------------------------------
4
+ * Drop-in bug reporting: detect screenshots, collect device/app/user context,
5
+ * and ship everything to Supabase (Postgres + Storage). A Database Webhook →
6
+ * Edge Function sends the notification email.
7
+ *
8
+ * Quick start:
9
+ *
10
+ * import { BugReporterProvider } from 'react-native-bug-reporter';
11
+ *
12
+ * <BugReporterProvider config={{ supabaseUrl: '...', supabaseAnonKey: '...' }}>
13
+ * <App />
14
+ * </BugReporterProvider>
15
+ */
16
+
17
+ export { BugReporterProvider } from './BugReporterProvider';
18
+ export { useBugReporter } from './context/BugReporterContext';
19
+ export { BugReportAdminScreen } from './components/BugReportAdminScreen';
20
+
21
+ // Navigation tracking helpers
22
+ export {
23
+ setCurrentScreen,
24
+ getCurrentScreen,
25
+ getActiveRouteName,
26
+ } from './navigation/screenTracker';
27
+
28
+ // Lower-level pieces, exported for advanced/custom integrations.
29
+ export { collectContext } from './collectors/collectContext';
30
+ export { submitBugReport } from './services/bugReportService';
31
+ export {
32
+ subscribeToReports,
33
+ updateReportStatus,
34
+ } from './services/supabaseService';
35
+ export { screenshotDetector } from './native/ScreenshotDetector';
36
+
37
+ export type {
38
+ BugReporterConfig,
39
+ BugReporterTheme,
40
+ BugReport,
41
+ BugReportInput,
42
+ BugSeverity,
43
+ CollectedContext,
44
+ DeviceInfo,
45
+ AppInfo,
46
+ NetworkInfo,
47
+ UserInfo,
48
+ ScreenshotData,
49
+ SubmitState,
50
+ } from './types';
51
+ export { SEVERITIES } from './types';