@koderlabs/tasks-sdk-rn-reporter 0.1.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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/modal.tsx","../src/annotator.tsx","../src/shake.ts","../src/screenshot-detection.ts","../src/transport.ts"],"sourcesContent":["/**\n * @koderlabs/tasks-sdk-rn-reporter\n *\n * In-app bug reporter for React Native — pair with `@koderlabs/tasks-sdk-rn`.\n *\n * Usage:\n * import { init } from '@koderlabs/tasks-sdk-rn';\n * import { initReporter } from '@koderlabs/tasks-sdk-rn-reporter';\n *\n * const rootRef = useRef<View>(null);\n * const sdk = init({ projectId: 'FE', accessKey: 'sk_…' });\n * const reporter = initReporter(sdk, {\n * captureRef: rootRef,\n * enableShakeGesture: true,\n * });\n * // reporter.show() — open modal manually\n */\n\nimport * as React from 'react';\n\nimport { BugReportModal } from './modal';\nimport { installShakeListener } from './shake';\nimport { installScreenshotDetection } from './screenshot-detection';\nimport { submitBugReport, type ReporterClient } from './transport';\nimport type { InitReporterOptions, ReporterHandle, BugReportResult, DrawCommand } from './types';\n\nexport type { InitReporterOptions, ReporterHandle, BugReportResult, DrawCommand } from './types';\nexport { submitBugReport } from './transport';\nexport { Annotator } from './annotator';\nexport type { AnnotatorProps, AnnotatorTool } from './annotator';\n\n/**\n * Initialise the bug reporter.\n *\n * Returns a handle whose `show()` you can wire to a button (FAB, menu item,\n * keyboard shortcut, etc.) and whose `close()` you should call on unmount.\n *\n * The reporter renders its own `<Modal>` via React Native's portal-like\n * built-in Modal component, so callers do NOT need to mount any extra\n * component in their tree.\n */\nexport function initReporter(\n client: ReporterClient,\n opts: InitReporterOptions = {},\n): ReporterHandle {\n // ── State held outside React because the host app may not render us ───\n let isOpen = false;\n let submitting = false;\n let screenshotUri: string | undefined;\n let rerender: (() => void) | null = null;\n let teardowns: Array<() => void> = [];\n\n // RN access — lazy require so the package can be loaded in node tests.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n let RN: any = null;\n try { RN = require('react-native'); } catch { /* test env */ }\n\n /** Capture the root view via view-shot (if available + ref provided). */\n async function captureScreenshot(): Promise<string | undefined> {\n if (!opts.captureRef?.current) return undefined;\n try {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const viewShot = require('react-native-view-shot');\n const uri: string = await viewShot.captureRef(opts.captureRef, {\n format: 'png',\n quality: 0.9,\n result: 'tmpfile',\n });\n return uri;\n } catch {\n // Module not installed or capture failed — fall through to text-only.\n return undefined;\n }\n }\n\n async function openModal() {\n if (isOpen || submitting) return;\n // Capture FIRST, then open — keeps the modal out of the screenshot.\n screenshotUri = await captureScreenshot();\n isOpen = true;\n rerender?.();\n }\n\n function closeModal() {\n if (!isOpen) return;\n isOpen = false;\n screenshotUri = undefined;\n rerender?.();\n }\n\n async function handleSubmit(data: { description: string; email?: string; annotations?: DrawCommand[] }): Promise<void> {\n submitting = true;\n rerender?.();\n try {\n const result: BugReportResult = await submitBugReport(client, {\n description: data.description,\n email: data.email,\n screenshotUri,\n annotations: data.annotations,\n defaultIssueTypeId: opts.defaultIssueTypeId,\n defaultPriorityId: opts.defaultPriorityId,\n customData: opts.customData,\n });\n submitting = false;\n isOpen = false;\n screenshotUri = undefined;\n rerender?.();\n if (!opts.silent && RN?.Alert) {\n RN.Alert.alert('Thanks!', `Bug report submitted (${result.ticketKey}).`);\n }\n } catch (err) {\n submitting = false;\n rerender?.();\n if (!opts.silent && RN?.Alert) {\n const msg = err instanceof Error ? err.message : 'Failed to submit';\n RN.Alert.alert(\n 'Could not send report',\n msg,\n [\n { text: 'Cancel', style: 'cancel', onPress: closeModal },\n { text: 'Retry', onPress: () => handleSubmit(data) },\n ],\n );\n }\n }\n }\n\n // ── Mount the modal once via a hidden root, if RN is available ──────\n // We use AppRegistry-less mounting via a Higher-Order React element that\n // is rendered into a portal by the host React tree. The simplest and\n // most reliable approach in pure JS is to register a root component, but\n // doing so without an entry-point hook is brittle. Instead we expose a\n // component the host CAN mount, and ALSO try the auto-mount path.\n //\n // Auto-mount path (preferred): use RN's `AppRegistry.setWrapperComponentProvider`\n // if available, otherwise rely on the developer adding `<ReporterRoot />`.\n //\n // For MVP we go with the explicit-mount approach via AppRegistry root tag\n // injection: register a separate root view. This works for Expo Router\n // and bare RN apps alike because React Native supports multiple root tags.\n //\n // …however that requires native plumbing on Android (createReactRootView).\n // To keep the MVP single-file we instead use a globally-shared singleton\n // that the host app can pull in via `<ReporterRoot />`. If the host doesn't\n // mount it, `show()` still works — we fall back to capturing + submitting\n // via Alert.prompt on iOS (Android: silent submit only).\n\n // Singleton state hook for the optional <ReporterRoot /> component.\n reporterSingleton = {\n isOpen: () => isOpen,\n submitting: () => submitting,\n screenshotUri: () => screenshotUri,\n collectEmail: opts.collectEmail,\n onCancel: closeModal,\n onSubmit: (data) => { void handleSubmit(data); },\n register: (cb) => { rerender = cb; },\n unregister: () => { rerender = null; },\n };\n\n // ── Auto-instrumentation ────────────────────────────────────────────\n if (opts.enableShakeGesture) {\n teardowns.push(installShakeListener(() => { void openModal(); }));\n }\n if (opts.enableScreenshotDetection) {\n teardowns.push(installScreenshotDetection(() => { void openModal(); }));\n }\n\n const handle: ReporterHandle = {\n show() { void openModal(); },\n hide() { closeModal(); },\n close() {\n closeModal();\n for (const fn of teardowns) { try { fn(); } catch { /* swallow */ } }\n teardowns = [];\n reporterSingleton = null;\n },\n };\n return handle;\n}\n\n// ── Optional <ReporterRoot /> component for hosts that want the modal ──\n// to render through their React tree instead of the auto-singleton path.\n//\n// Usage:\n// export default function App() {\n// return (<>\n// <NavigationContainer>…</NavigationContainer>\n// <ReporterRoot />\n// </>);\n// }\n//\n// Mount once at the top of your app. Safe to omit on RN versions where the\n// builtin `<Modal>` works without an explicit React root injection (most\n// modern RN apps).\n\ntype Singleton = {\n isOpen(): boolean;\n submitting(): boolean;\n screenshotUri(): string | undefined;\n collectEmail?: boolean;\n onCancel(): void;\n onSubmit(data: { description: string; email?: string; annotations?: DrawCommand[] }): void;\n register(cb: () => void): void;\n unregister(): void;\n};\n\nlet reporterSingleton: Singleton | null = null;\n\nexport function ReporterRoot(): React.ReactElement | null {\n const [, setTick] = React.useState(0);\n const force = React.useCallback(() => setTick((t) => t + 1), []);\n\n React.useEffect(() => {\n if (!reporterSingleton) return;\n reporterSingleton.register(force);\n return () => { reporterSingleton?.unregister(); };\n }, [force]);\n\n if (!reporterSingleton) return null;\n return React.createElement(BugReportModal, {\n visible: reporterSingleton.isOpen(),\n submitting: reporterSingleton.submitting(),\n screenshotUri: reporterSingleton.screenshotUri(),\n collectEmail: reporterSingleton.collectEmail,\n onCancel: reporterSingleton.onCancel,\n onSubmit: reporterSingleton.onSubmit,\n });\n}\n\nexport default initReporter;\n","/**\n * Bug-report modal for React Native.\n *\n * Renders a full-screen RN `<Modal>` with a description field, optional email\n * field, screenshot thumbnail (if one was captured), and Cancel / Submit.\n *\n * The component does NOT capture the screenshot itself — by the time the\n * modal is mounted, the caller in `index.ts` has already snapped the root\n * view via `react-native-view-shot`. This guarantees the modal chrome never\n * appears in the captured image.\n */\n\nimport * as React from 'react';\n\nimport { Annotator } from './annotator';\nimport type { DrawCommand } from './types';\n\n// We deliberately require react-native lazily through a guarded require so the\n// package can still be imported in a node test environment without RN.\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nlet RN: any;\ntry { RN = require('react-native'); } catch { RN = null; }\n\nexport interface BugReportModalProps {\n visible: boolean;\n screenshotUri?: string;\n collectEmail?: boolean;\n defaultEmail?: string;\n submitting: boolean;\n onCancel: () => void;\n onSubmit: (data: { description: string; email?: string; annotations?: DrawCommand[] }) => void;\n}\n\nexport function BugReportModal(props: BugReportModalProps): React.ReactElement | null {\n const [description, setDescription] = React.useState('');\n const [email, setEmail] = React.useState(props.defaultEmail ?? '');\n const [annotations, setAnnotations] = React.useState<DrawCommand[]>([]);\n const [annotating, setAnnotating] = React.useState(false);\n\n // Reset state each time the modal opens.\n React.useEffect(() => {\n if (props.visible) {\n setDescription('');\n setEmail(props.defaultEmail ?? '');\n setAnnotations([]);\n setAnnotating(false);\n }\n }, [props.visible, props.defaultEmail]);\n\n if (!RN) return null;\n const { Modal, View, Text, TextInput, TouchableOpacity, Image, ScrollView, ActivityIndicator } = RN;\n\n const canSubmit = description.trim().length > 0 && !props.submitting;\n\n return (\n <Modal\n visible={props.visible}\n animationType=\"slide\"\n transparent={false}\n onRequestClose={props.onCancel}\n >\n <View style={styles.container}>\n <View style={styles.header}>\n <Text style={styles.title}>Report a bug</Text>\n <TouchableOpacity onPress={props.onCancel} accessibilityRole=\"button\">\n <Text style={styles.cancelBtn}>Cancel</Text>\n </TouchableOpacity>\n </View>\n\n <ScrollView contentContainerStyle={styles.body} keyboardShouldPersistTaps=\"handled\">\n {props.screenshotUri ? (\n <View>\n <Image\n source={{ uri: props.screenshotUri }}\n style={styles.screenshot}\n resizeMode=\"contain\"\n />\n <View style={styles.screenshotActions}>\n <TouchableOpacity\n onPress={() => setAnnotating(true)}\n style={styles.annotateBtn}\n accessibilityRole=\"button\"\n disabled={props.submitting}\n >\n <Text style={styles.annotateBtnText}>\n {annotations.length > 0\n ? `Annotate (${annotations.length})`\n : 'Annotate'}\n </Text>\n </TouchableOpacity>\n </View>\n </View>\n ) : (\n <View style={styles.placeholder}>\n <Text style={styles.placeholderText}>No screenshot</Text>\n </View>\n )}\n\n <Text style={styles.label}>What went wrong?</Text>\n <TextInput\n style={styles.descriptionInput}\n value={description}\n onChangeText={setDescription}\n placeholder=\"Describe what you expected vs. what happened…\"\n multiline\n numberOfLines={5}\n autoFocus\n editable={!props.submitting}\n />\n\n {props.collectEmail && (\n <>\n <Text style={styles.label}>Your email (optional)</Text>\n <TextInput\n style={styles.emailInput}\n value={email}\n onChangeText={setEmail}\n placeholder=\"you@example.com\"\n autoCapitalize=\"none\"\n keyboardType=\"email-address\"\n editable={!props.submitting}\n />\n </>\n )}\n </ScrollView>\n\n <View style={styles.footer}>\n <TouchableOpacity\n disabled={!canSubmit}\n onPress={() => props.onSubmit({\n description: description.trim(),\n email: email.trim() || undefined,\n annotations: annotations.length > 0 ? annotations : undefined,\n })}\n style={[styles.submitBtn, !canSubmit && styles.submitBtnDisabled]}\n accessibilityRole=\"button\"\n >\n {props.submitting\n ? <ActivityIndicator color=\"#fff\" />\n : <Text style={styles.submitBtnText}>Send</Text>}\n </TouchableOpacity>\n </View>\n\n {/* Annotator overlay — full-screen Modal sitting above the bug-report\n modal. Mounted only when active so we don't pay react-native-svg\n cost on every modal open. */}\n {annotating && props.screenshotUri && (\n <Modal\n visible\n animationType=\"fade\"\n transparent={false}\n onRequestClose={() => setAnnotating(false)}\n >\n <Annotator\n imageUri={props.screenshotUri}\n onCommit={(cmds) => { setAnnotations(cmds); setAnnotating(false); }}\n onCancel={() => setAnnotating(false)}\n />\n </Modal>\n )}\n </View>\n </Modal>\n );\n}\n\n// Inline StyleSheet — small surface; keeping styles next to markup makes the\n// component drop-in for unfamiliar codebases. Real apps can re-skin by\n// forking this file (the rest of the package stays usable).\nconst styles = RN\n ? RN.StyleSheet.create({\n container: { flex: 1, backgroundColor: '#fff' },\n header: {\n flexDirection: 'row',\n justifyContent: 'space-between',\n alignItems: 'center',\n padding: 16,\n borderBottomWidth: 1,\n borderBottomColor: '#e5e7eb',\n },\n title: { fontSize: 18, fontWeight: '600', color: '#111827' },\n cancelBtn: { fontSize: 16, color: '#6b7280' },\n body: { padding: 16, paddingBottom: 32 },\n screenshot: {\n width: '100%',\n height: 240,\n backgroundColor: '#f3f4f6',\n borderRadius: 8,\n marginBottom: 16,\n },\n placeholder: {\n width: '100%',\n height: 120,\n backgroundColor: '#f3f4f6',\n borderRadius: 8,\n marginBottom: 16,\n alignItems: 'center',\n justifyContent: 'center',\n },\n placeholderText: { color: '#9ca3af' },\n screenshotActions: { flexDirection: 'row', justifyContent: 'flex-end', marginTop: -8, marginBottom: 16 },\n annotateBtn: {\n paddingHorizontal: 12,\n paddingVertical: 6,\n borderRadius: 6,\n backgroundColor: '#eef2ff',\n },\n annotateBtnText: { color: '#4f46e5', fontSize: 13, fontWeight: '600' },\n label: { fontSize: 14, fontWeight: '500', color: '#374151', marginBottom: 6, marginTop: 8 },\n descriptionInput: {\n borderWidth: 1,\n borderColor: '#d1d5db',\n borderRadius: 8,\n padding: 12,\n fontSize: 15,\n minHeight: 120,\n textAlignVertical: 'top',\n color: '#111827',\n backgroundColor: '#fff',\n },\n emailInput: {\n borderWidth: 1,\n borderColor: '#d1d5db',\n borderRadius: 8,\n padding: 12,\n fontSize: 15,\n color: '#111827',\n backgroundColor: '#fff',\n },\n footer: { padding: 16, borderTopWidth: 1, borderTopColor: '#e5e7eb' },\n submitBtn: {\n backgroundColor: '#4f46e5',\n borderRadius: 8,\n padding: 14,\n alignItems: 'center',\n },\n submitBtnDisabled: { backgroundColor: '#a5b4fc' },\n submitBtnText: { color: '#fff', fontSize: 16, fontWeight: '600' },\n })\n : ({} as any);\n","/**\n * RN Annotator — box highlight + redact tool overlay for the captured screenshot.\n *\n * Mirrors the web-reporter `DrawCommand` shape (normalized 0..1 coords against\n * the captured image dimensions) so the backend can render baked annotations\n * uniformly across platforms.\n *\n * v1: emits `DrawCommand[]` only — the transport ships them alongside the raw\n * screenshot. Server-side compositing is deferred (see README).\n *\n * Uses `react-native-svg` (peer dep, optional) for vector overlay rendering\n * and RN's built-in `PanResponder` for tap-drag gestures — no extra gesture\n * library required.\n */\n\nimport * as React from 'react';\n\nimport type { DrawCommand } from './types';\n\n// Lazy requires so the package can be loaded in node test envs.\n/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-var-requires */\nlet RN: any = null;\ntry { RN = require('react-native'); } catch { /* test env */ }\n\nlet Svg: any = null;\nlet SvgRect: any = null;\nlet SvgG: any = null;\nlet SvgImage: any = null;\ntry {\n const mod = require('react-native-svg');\n Svg = mod.default ?? mod.Svg;\n SvgRect = mod.Rect;\n SvgG = mod.G;\n SvgImage = mod.Image;\n} catch { /* peer dep not installed */ }\n/* eslint-enable */\n\nexport type AnnotatorTool = 'highlight' | 'hide';\n\nexport interface AnnotatorProps {\n /** file:// URI of the captured screenshot. */\n imageUri: string;\n /** Called when user taps \"Done\" — receives the list of normalized commands. */\n onCommit: (commands: DrawCommand[]) => void;\n /** Called when user taps \"Cancel\". */\n onCancel: () => void;\n}\n\ninterface PixelRect { x: number; y: number; w: number; h: number }\n\n/**\n * The annotator renders the captured screenshot full-screen with a transparent\n * SVG overlay that captures pan gestures and translates them into committed\n * `DrawCommand`s. Tool palette lives at the bottom with Undo / Clear / Done.\n */\nexport function Annotator(props: AnnotatorProps): React.ReactElement | null {\n const [tool, setTool] = React.useState<AnnotatorTool>('highlight');\n const [commands, setCommands] = React.useState<DrawCommand[]>([]);\n const [draftRect, setDraftRect] = React.useState<PixelRect | null>(null);\n\n // Canvas dimensions (pixels of the visible overlay — used to normalize coords).\n // Filled by the View's onLayout. Until then, gesture is a no-op.\n const layoutRef = React.useRef<{ w: number; h: number }>({ w: 0, h: 0 });\n const startRef = React.useRef<{ x: number; y: number } | null>(null);\n\n // Pan responder must be created lazily after RN is available.\n const panResponder = React.useMemo(() => {\n if (!RN) return null;\n return RN.PanResponder.create({\n onStartShouldSetPanResponder: () => true,\n onMoveShouldSetPanResponder: () => true,\n onPanResponderGrant: (_e: unknown, gestureState: { x0: number; y0: number }) => {\n // locationX/Y from the event isn't reliable across RN versions for the\n // drag-start coords, so use gestureState's absolute coords and assume the\n // overlay fills the screen edge-to-edge horizontally.\n startRef.current = { x: gestureState.x0, y: gestureState.y0 };\n setDraftRect({ x: gestureState.x0, y: gestureState.y0, w: 0, h: 0 });\n },\n onPanResponderMove: (_e: unknown, gestureState: { moveX: number; moveY: number }) => {\n const start = startRef.current;\n if (!start) return;\n setDraftRect(buildPixelRect(start, { x: gestureState.moveX, y: gestureState.moveY }));\n },\n onPanResponderRelease: (_e: unknown, gestureState: { moveX: number; moveY: number; x0: number; y0: number }) => {\n const start = startRef.current;\n startRef.current = null;\n if (!start) { setDraftRect(null); return; }\n // moveX/Y can be 0 if the user just tapped without moving — fall back to start.\n const endX = gestureState.moveX || start.x;\n const endY = gestureState.moveY || start.y;\n const rect = buildPixelRect(start, { x: endX, y: endY });\n setDraftRect(null);\n // Discard trivial taps (< 8px in either dimension).\n if (rect.w < 8 || rect.h < 8) return;\n const { w: W, h: H } = layoutRef.current;\n if (W === 0 || H === 0) return;\n setCommands((prev) => [\n ...prev,\n {\n type: tool,\n x: clamp01(rect.x / W),\n y: clamp01(rect.y / H),\n w: clamp01(rect.w / W),\n h: clamp01(rect.h / H),\n },\n ]);\n },\n onPanResponderTerminate: () => {\n startRef.current = null;\n setDraftRect(null);\n },\n });\n }, [tool]);\n\n if (!RN) return null;\n const { View, Text, TouchableOpacity, Image } = RN;\n\n function onLayout(e: { nativeEvent: { layout: { width: number; height: number } } }) {\n layoutRef.current = { w: e.nativeEvent.layout.width, h: e.nativeEvent.layout.height };\n }\n\n function undo() { setCommands((prev) => prev.slice(0, -1)); }\n function clear() { setCommands([]); }\n\n // Renderable rects (committed + draft preview) in pixel space for SVG.\n const { w: W, h: H } = layoutRef.current;\n const pixelCommands = (W > 0 && H > 0)\n ? commands.map((c) => ({\n type: c.type,\n x: c.x * W, y: c.y * H, w: c.w * W, h: c.h * H,\n }))\n : [];\n\n return (\n <View style={styles.root}>\n <View\n style={styles.canvas}\n onLayout={onLayout}\n {...(panResponder?.panHandlers ?? {})}\n >\n <Image\n source={{ uri: props.imageUri }}\n style={styles.image}\n resizeMode=\"contain\"\n />\n {Svg && W > 0 && H > 0 && (\n <Svg width={W} height={H} style={styles.svg} pointerEvents=\"none\">\n <SvgG>\n {pixelCommands.map((cmd, i) => renderRect(cmd, i))}\n {draftRect && renderRect({ type: tool, ...draftRect }, -1)}\n </SvgG>\n </Svg>\n )}\n </View>\n\n {/* Tool palette */}\n <View style={styles.toolbar}>\n <View style={styles.toolGroup}>\n <ToolButton\n label=\"Highlight\"\n active={tool === 'highlight'}\n onPress={() => setTool('highlight')}\n />\n <ToolButton\n label=\"Redact\"\n active={tool === 'hide'}\n onPress={() => setTool('hide')}\n />\n </View>\n <View style={styles.toolGroup}>\n <TouchableOpacity\n onPress={undo}\n disabled={commands.length === 0}\n style={[styles.iconBtn, commands.length === 0 && styles.iconBtnDisabled]}\n accessibilityRole=\"button\"\n accessibilityLabel=\"Undo\"\n >\n <Text style={styles.iconBtnText}>Undo</Text>\n </TouchableOpacity>\n <TouchableOpacity\n onPress={clear}\n disabled={commands.length === 0}\n style={[styles.iconBtn, commands.length === 0 && styles.iconBtnDisabled]}\n accessibilityRole=\"button\"\n accessibilityLabel=\"Clear all\"\n >\n <Text style={styles.iconBtnText}>Clear</Text>\n </TouchableOpacity>\n </View>\n </View>\n\n {/* Action bar */}\n <View style={styles.actions}>\n <TouchableOpacity\n onPress={props.onCancel}\n style={styles.cancelBtn}\n accessibilityRole=\"button\"\n >\n <Text style={styles.cancelBtnText}>Cancel</Text>\n </TouchableOpacity>\n <TouchableOpacity\n onPress={() => props.onCommit(commands)}\n style={styles.doneBtn}\n accessibilityRole=\"button\"\n >\n <Text style={styles.doneBtnText}>Done</Text>\n </TouchableOpacity>\n </View>\n </View>\n );\n}\n\nfunction ToolButton(props: { label: string; active: boolean; onPress: () => void }) {\n const { TouchableOpacity, Text } = RN;\n return (\n <TouchableOpacity\n onPress={props.onPress}\n style={[styles.toolBtn, props.active && styles.toolBtnActive]}\n accessibilityRole=\"button\"\n accessibilityState={{ selected: props.active }}\n >\n <Text style={[styles.toolBtnText, props.active && styles.toolBtnTextActive]}>\n {props.label}\n </Text>\n </TouchableOpacity>\n );\n}\n\n/** Render one SVG rect per command. Highlight = red stroke; Redact = solid black fill. */\nfunction renderRect(cmd: { type: AnnotatorTool; x: number; y: number; w: number; h: number }, key: number) {\n if (!SvgRect) return null;\n const isRedact = cmd.type === 'hide';\n return (\n <SvgRect\n key={key}\n x={cmd.x}\n y={cmd.y}\n width={cmd.w}\n height={cmd.h}\n fill={isRedact ? '#000000' : 'rgba(220, 38, 38, 0.15)'}\n stroke={isRedact ? '#000000' : '#dc2626'}\n strokeWidth={isRedact ? 0 : 3}\n />\n );\n}\n\n/**\n * Build a normalized `DrawCommand` from a drag start, drag end, the active tool,\n * and the canvas pixel dimensions. Pure — exported for unit tests.\n *\n * Returns `null` when the drag is below the minimum size threshold (8px in\n * either dimension) — caller should discard.\n */\nexport function buildDrawCommand(\n start: { x: number; y: number },\n end: { x: number; y: number },\n tool: AnnotatorTool,\n canvas: { w: number; h: number },\n): DrawCommand | null {\n if (canvas.w === 0 || canvas.h === 0) return null;\n const rect = buildPixelRect(start, end);\n if (rect.w < 8 || rect.h < 8) return null;\n return {\n type: tool,\n x: clamp01(rect.x / canvas.w),\n y: clamp01(rect.y / canvas.h),\n w: clamp01(rect.w / canvas.w),\n h: clamp01(rect.h / canvas.h),\n };\n}\n\nfunction buildPixelRect(a: { x: number; y: number }, b: { x: number; y: number }): PixelRect {\n return {\n x: Math.min(a.x, b.x),\n y: Math.min(a.y, b.y),\n w: Math.abs(b.x - a.x),\n h: Math.abs(b.y - a.y),\n };\n}\n\nfunction clamp01(v: number): number {\n if (v < 0) return 0;\n if (v > 1) return 1;\n return v;\n}\n\nconst styles = RN\n ? RN.StyleSheet.create({\n root: { flex: 1, backgroundColor: '#111827' },\n canvas: { flex: 1, position: 'relative' },\n image: { ...RN.StyleSheet.absoluteFillObject },\n svg: { ...RN.StyleSheet.absoluteFillObject },\n toolbar: {\n flexDirection: 'row',\n justifyContent: 'space-between',\n paddingHorizontal: 16,\n paddingVertical: 12,\n backgroundColor: '#1f2937',\n borderTopWidth: 1,\n borderTopColor: '#374151',\n },\n toolGroup: { flexDirection: 'row', gap: 8 },\n toolBtn: {\n paddingHorizontal: 14,\n paddingVertical: 8,\n borderRadius: 8,\n backgroundColor: '#374151',\n },\n toolBtnActive: { backgroundColor: '#4f46e5' },\n toolBtnText: { color: '#d1d5db', fontSize: 14, fontWeight: '500' },\n toolBtnTextActive: { color: '#fff' },\n iconBtn: {\n paddingHorizontal: 12,\n paddingVertical: 8,\n borderRadius: 8,\n backgroundColor: '#374151',\n },\n iconBtnDisabled: { opacity: 0.4 },\n iconBtnText: { color: '#d1d5db', fontSize: 14 },\n actions: {\n flexDirection: 'row',\n gap: 12,\n padding: 16,\n backgroundColor: '#1f2937',\n borderTopWidth: 1,\n borderTopColor: '#374151',\n },\n cancelBtn: {\n flex: 1,\n paddingVertical: 14,\n borderRadius: 8,\n backgroundColor: '#374151',\n alignItems: 'center',\n },\n cancelBtnText: { color: '#fff', fontSize: 16, fontWeight: '500' },\n doneBtn: {\n flex: 1,\n paddingVertical: 14,\n borderRadius: 8,\n backgroundColor: '#4f46e5',\n alignItems: 'center',\n },\n doneBtnText: { color: '#fff', fontSize: 16, fontWeight: '600' },\n })\n : ({} as any);\n\nexport default Annotator;\n","/**\n * Shake-to-report gesture.\n *\n * iOS: React Native fires the `NativeAppEventEmitter` event\n * `'AccelerometerSecondaryShake'` when iOS detects a shake. Older docs\n * reference the `'shake'` event on DeviceEventEmitter — we listen to both\n * to cover the spread of RN versions.\n *\n * Android: RN does not expose shake natively. Apps wanting Android coverage\n * should install `expo-sensors` — if present we poll the accelerometer at\n * ~50ms and apply a simple magnitude threshold. Otherwise: no-op.\n */\n\n/** Stops listening; safe to call multiple times. */\nexport type ShakeTeardown = () => void;\n\nconst SHAKE_THRESHOLD = 2.7; // g — slightly above brisk hand motion\nconst SHAKE_COOLDOWN_MS = 1500; // suppress repeats while modal opens\n\nexport function installShakeListener(onShake: () => void): ShakeTeardown {\n const teardowns: Array<() => void> = [];\n let lastFiredAt = 0;\n\n const fire = () => {\n const now = Date.now();\n if (now - lastFiredAt < SHAKE_COOLDOWN_MS) return;\n lastFiredAt = now;\n try { onShake(); } catch { /* swallow */ }\n };\n\n // iOS path — bind both legacy + modern event names.\n try {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const RN = require('react-native');\n const emitter = RN.DeviceEventEmitter ?? RN.NativeAppEventEmitter;\n if (emitter?.addListener) {\n const subs = [\n emitter.addListener('AccelerometerSecondaryShake', fire),\n emitter.addListener('shake', fire),\n ];\n teardowns.push(() => subs.forEach((s: { remove?: () => void }) => s?.remove?.()));\n }\n } catch {\n /* react-native not available — running under Node tests */\n }\n\n // Android (and iOS) — optional expo-sensors path.\n try {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const sensors = require('expo-sensors');\n if (sensors?.Accelerometer) {\n sensors.Accelerometer.setUpdateInterval(50);\n const sub = sensors.Accelerometer.addListener(\n ({ x, y, z }: { x: number; y: number; z: number }) => {\n const magnitude = Math.sqrt(x * x + y * y + z * z);\n if (magnitude > SHAKE_THRESHOLD) fire();\n },\n );\n teardowns.push(() => sub?.remove?.());\n }\n } catch {\n /* expo-sensors not installed — Android shake disabled */\n }\n\n return () => {\n for (const t of teardowns) { try { t(); } catch { /* ignore */ } }\n };\n}\n","/**\n * iOS screenshot-detection prompt.\n *\n * Uses `expo-screen-capture.addScreenshotListener` — fires whenever the user\n * presses Side+Volume-Up. We then show an `Alert.alert` offering to open the\n * reporter pre-filled with that screenshot.\n *\n * Note: iOS does not let us read the actual screenshot pixels (sandboxed in\n * the Photos library), so we just open the reporter and let view-shot capture\n * the current view on `onYes`.\n *\n * Android: there's no public, reliable system hook for screenshot capture\n * across OEMs. This module is a no-op on Android.\n */\n\nexport type ScreenshotDetectionTeardown = () => void;\n\nexport function installScreenshotDetection(\n onUserAccepted: () => void,\n): ScreenshotDetectionTeardown {\n let RN: typeof import('react-native') | null = null;\n try { RN = require('react-native'); } catch { /* not in RN env */ }\n\n if (!RN || RN.Platform.OS !== 'ios') return () => {};\n\n let capture: typeof import('expo-screen-capture') | null = null;\n try { capture = require('expo-screen-capture'); } catch {\n if (__DEV__) {\n console.warn(\n '[InstantTasks reporter] enableScreenshotDetection requires expo-screen-capture; install it or disable the option.',\n );\n }\n return () => {};\n }\n\n const Alert = RN.Alert;\n const sub = capture.addScreenshotListener(() => {\n Alert.alert(\n 'Report a problem?',\n 'You just took a screenshot. Send it as a bug report?',\n [\n { text: 'No', style: 'cancel' },\n { text: 'Yes', onPress: () => { try { onUserAccepted(); } catch { /* swallow */ } } },\n ],\n { cancelable: true },\n );\n });\n\n return () => { try { sub?.remove?.(); } catch { /* ignore */ } };\n}\n\ndeclare const __DEV__: boolean;\n","/**\n * Reporter transport — wraps `client.send()` for `kind: 'bug_report'` events.\n *\n * The core SDK already routes events that carry attachments through the\n * `/sdk/events/multipart` ingest endpoint, so all we have to do is build the\n * event payload + (optional) screenshot Blob and call `client.send()`.\n */\n\nimport type { Client } from '@koderlabs/tasks-sdk';\nimport type { BugReportResult, BugReportSubmission } from './types';\n\n/** Either the bare core Client or an `RnClientHandle` from sdk-rn. */\nexport type ReporterClient = Client | { client: Client };\n\nfunction unwrap(c: ReporterClient): Client {\n return (c as { client?: Client }).client ?? (c as Client);\n}\n\n/**\n * Read a `file://` URI returned by react-native-view-shot and convert it\n * to a PNG Blob suitable for multipart upload.\n *\n * RN's global `fetch()` accepts `file://` URIs and returns a Response whose\n * `.blob()` produces a Blob backed by the file's bytes — this works on both\n * iOS and Android (Hermes + JSC).\n */\nasync function fileUriToBlob(uri: string): Promise<Blob | null> {\n try {\n const res = await fetch(uri);\n return await res.blob();\n } catch {\n return null;\n }\n}\n\n/**\n * Build the bug_report event + attachments map and submit via client.send().\n */\nexport async function submitBugReport(\n c: ReporterClient,\n submission: BugReportSubmission,\n): Promise<BugReportResult> {\n const client = unwrap(c);\n\n // Collect minimal RN metadata. We deliberately keep this small — the sdk-rn\n // client already attaches platform context to every event server-side.\n const Platform: { OS?: string; Version?: string | number } =\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n (() => {\n try { return require('react-native').Platform ?? {}; }\n catch { return {}; }\n })();\n\n const event = {\n kind: 'bug_report' as const,\n ts: new Date().toISOString(),\n platform: {\n os: Platform.OS,\n version: Platform.Version,\n },\n payload: {\n description: submission.description,\n email: submission.email,\n defaultIssueTypeId: submission.defaultIssueTypeId,\n defaultPriorityId: submission.defaultPriorityId,\n annotations: submission.annotations,\n metadata: {\n customData: submission.customData,\n },\n },\n };\n\n const attachments = new Map<string, Blob>();\n if (submission.screenshotUri) {\n const blob = await fileUriToBlob(submission.screenshotUri);\n if (blob) attachments.set('screenshot', blob);\n }\n\n // `client.send()` returns a generic result; the multipart endpoint enriches\n // it with `ticketKey` for bug_report events.\n const result = await client.send(\n event as unknown as Parameters<Client['send']>[0],\n attachments.size > 0 ? attachments : undefined,\n );\n\n const ticketKey = (result as { ticketKey?: string }).ticketKey ?? 'unknown';\n const baseUrl = client.options.endpoint?.replace(/\\/+$/, '') ?? '';\n const ticketUrl = `${baseUrl}/tickets/${ticketKey}`;\n\n return { ticketKey, ticketUrl };\n}\n"],"mappings":"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","names":["React","React","W","H","import_jsx_runtime","RN","styles","RN","RN","RN"]}
@@ -0,0 +1,175 @@
1
+ import * as React from 'react';
2
+ import { RefObject } from 'react';
3
+ import { Client } from '@koderlabs/tasks-sdk';
4
+
5
+ /**
6
+ * Annotator draw command — normalized 0..1 against the captured image dimensions.
7
+ *
8
+ * Shape mirrors `@koderlabs/tasks-sdk-web-reporter` so the backend can render
9
+ * baked annotations uniformly across web + RN. Duplicated here (not imported)
10
+ * because cross-package imports into RN would force the web-reporter into the
11
+ * Metro bundle tree and pull in DOM-only deps.
12
+ */
13
+ interface DrawCommand {
14
+ /** 'highlight' = red outlined rect; 'hide' = solid black redaction box. */
15
+ type: 'highlight' | 'hide';
16
+ /** Normalized 0..1 x-origin against captured image width. */
17
+ x: number;
18
+ /** Normalized 0..1 y-origin against captured image height. */
19
+ y: number;
20
+ /** Normalized 0..1 width. */
21
+ w: number;
22
+ /** Normalized 0..1 height. */
23
+ h: number;
24
+ }
25
+ /**
26
+ * Options for initialising the React Native bug reporter.
27
+ *
28
+ * The reporter is a paired add-on for `@koderlabs/tasks-sdk-rn` — pass either
29
+ * the bare core `Client` or the `RnClientHandle` returned by `init()`.
30
+ */
31
+ interface InitReporterOptions {
32
+ /**
33
+ * Ref to the root `<View>` that should be captured into a screenshot.
34
+ * Required for screenshot capture; if omitted, reports submit text-only.
35
+ *
36
+ * The capture is taken *before* the modal is presented so the modal
37
+ * never appears in the screenshot.
38
+ */
39
+ captureRef?: RefObject<unknown>;
40
+ /**
41
+ * Listen for device shake gestures and auto-open the reporter on shake.
42
+ * iOS: uses the built-in shake event. Android: requires `expo-sensors`
43
+ * to be installed (graceful no-op otherwise).
44
+ * Default: false.
45
+ */
46
+ enableShakeGesture?: boolean;
47
+ /**
48
+ * On iOS, show an `Alert.alert("Send screenshot?")` prompt whenever the
49
+ * user takes a system screenshot. Requires `expo-screen-capture`.
50
+ * Android: no-op (OS does not expose a reliable hook).
51
+ * Default: false.
52
+ */
53
+ enableScreenshotDetection?: boolean;
54
+ /** Default issue type id applied to created tickets. */
55
+ defaultIssueTypeId?: string;
56
+ /** Default priority id applied to created tickets. */
57
+ defaultPriorityId?: string;
58
+ /**
59
+ * Show an email field in the modal so anonymous reporters can identify
60
+ * themselves. If your app already authenticates users you can leave this
61
+ * off and pass the user's email via `client` user context elsewhere.
62
+ * Default: false.
63
+ */
64
+ collectEmail?: boolean;
65
+ /** Suppress success/error alerts. Default: false (alerts shown). */
66
+ silent?: boolean;
67
+ /** Extra custom data merged into every report's payload. */
68
+ customData?: Record<string, unknown>;
69
+ }
70
+ /** Imperative handle returned by `initReporter()`. */
71
+ interface ReporterHandle {
72
+ /** Capture a screenshot (if captureRef provided) then open the modal. */
73
+ show(): void;
74
+ /** Hide the modal without sending. Alias for close(). */
75
+ hide(): void;
76
+ /** Tear down listeners (shake, screenshot-detection) and hide modal. */
77
+ close(): void;
78
+ }
79
+ /** Payload submitted by the modal — internal. */
80
+ interface BugReportSubmission {
81
+ description: string;
82
+ email?: string;
83
+ screenshotUri?: string;
84
+ /** Annotator draw commands captured over the screenshot. v1: shipped raw; server-side bake pending. */
85
+ annotations?: DrawCommand[];
86
+ defaultIssueTypeId?: string;
87
+ defaultPriorityId?: string;
88
+ customData?: Record<string, unknown>;
89
+ }
90
+ /** Result returned from a successful submission. */
91
+ interface BugReportResult {
92
+ ticketKey: string;
93
+ ticketUrl: string;
94
+ }
95
+
96
+ /**
97
+ * Reporter transport — wraps `client.send()` for `kind: 'bug_report'` events.
98
+ *
99
+ * The core SDK already routes events that carry attachments through the
100
+ * `/sdk/events/multipart` ingest endpoint, so all we have to do is build the
101
+ * event payload + (optional) screenshot Blob and call `client.send()`.
102
+ */
103
+
104
+ /** Either the bare core Client or an `RnClientHandle` from sdk-rn. */
105
+ type ReporterClient = Client | {
106
+ client: Client;
107
+ };
108
+ /**
109
+ * Build the bug_report event + attachments map and submit via client.send().
110
+ */
111
+ declare function submitBugReport(c: ReporterClient, submission: BugReportSubmission): Promise<BugReportResult>;
112
+
113
+ /**
114
+ * RN Annotator — box highlight + redact tool overlay for the captured screenshot.
115
+ *
116
+ * Mirrors the web-reporter `DrawCommand` shape (normalized 0..1 coords against
117
+ * the captured image dimensions) so the backend can render baked annotations
118
+ * uniformly across platforms.
119
+ *
120
+ * v1: emits `DrawCommand[]` only — the transport ships them alongside the raw
121
+ * screenshot. Server-side compositing is deferred (see README).
122
+ *
123
+ * Uses `react-native-svg` (peer dep, optional) for vector overlay rendering
124
+ * and RN's built-in `PanResponder` for tap-drag gestures — no extra gesture
125
+ * library required.
126
+ */
127
+
128
+ type AnnotatorTool = 'highlight' | 'hide';
129
+ interface AnnotatorProps {
130
+ /** file:// URI of the captured screenshot. */
131
+ imageUri: string;
132
+ /** Called when user taps "Done" — receives the list of normalized commands. */
133
+ onCommit: (commands: DrawCommand[]) => void;
134
+ /** Called when user taps "Cancel". */
135
+ onCancel: () => void;
136
+ }
137
+ /**
138
+ * The annotator renders the captured screenshot full-screen with a transparent
139
+ * SVG overlay that captures pan gestures and translates them into committed
140
+ * `DrawCommand`s. Tool palette lives at the bottom with Undo / Clear / Done.
141
+ */
142
+ declare function Annotator(props: AnnotatorProps): React.ReactElement | null;
143
+
144
+ /**
145
+ * @koderlabs/tasks-sdk-rn-reporter
146
+ *
147
+ * In-app bug reporter for React Native — pair with `@koderlabs/tasks-sdk-rn`.
148
+ *
149
+ * Usage:
150
+ * import { init } from '@koderlabs/tasks-sdk-rn';
151
+ * import { initReporter } from '@koderlabs/tasks-sdk-rn-reporter';
152
+ *
153
+ * const rootRef = useRef<View>(null);
154
+ * const sdk = init({ projectId: 'FE', accessKey: 'sk_…' });
155
+ * const reporter = initReporter(sdk, {
156
+ * captureRef: rootRef,
157
+ * enableShakeGesture: true,
158
+ * });
159
+ * // reporter.show() — open modal manually
160
+ */
161
+
162
+ /**
163
+ * Initialise the bug reporter.
164
+ *
165
+ * Returns a handle whose `show()` you can wire to a button (FAB, menu item,
166
+ * keyboard shortcut, etc.) and whose `close()` you should call on unmount.
167
+ *
168
+ * The reporter renders its own `<Modal>` via React Native's portal-like
169
+ * built-in Modal component, so callers do NOT need to mount any extra
170
+ * component in their tree.
171
+ */
172
+ declare function initReporter(client: ReporterClient, opts?: InitReporterOptions): ReporterHandle;
173
+ declare function ReporterRoot(): React.ReactElement | null;
174
+
175
+ export { Annotator, type AnnotatorProps, type AnnotatorTool, type BugReportResult, type DrawCommand, type InitReporterOptions, type ReporterHandle, ReporterRoot, initReporter as default, initReporter, submitBugReport };
@@ -0,0 +1,175 @@
1
+ import * as React from 'react';
2
+ import { RefObject } from 'react';
3
+ import { Client } from '@koderlabs/tasks-sdk';
4
+
5
+ /**
6
+ * Annotator draw command — normalized 0..1 against the captured image dimensions.
7
+ *
8
+ * Shape mirrors `@koderlabs/tasks-sdk-web-reporter` so the backend can render
9
+ * baked annotations uniformly across web + RN. Duplicated here (not imported)
10
+ * because cross-package imports into RN would force the web-reporter into the
11
+ * Metro bundle tree and pull in DOM-only deps.
12
+ */
13
+ interface DrawCommand {
14
+ /** 'highlight' = red outlined rect; 'hide' = solid black redaction box. */
15
+ type: 'highlight' | 'hide';
16
+ /** Normalized 0..1 x-origin against captured image width. */
17
+ x: number;
18
+ /** Normalized 0..1 y-origin against captured image height. */
19
+ y: number;
20
+ /** Normalized 0..1 width. */
21
+ w: number;
22
+ /** Normalized 0..1 height. */
23
+ h: number;
24
+ }
25
+ /**
26
+ * Options for initialising the React Native bug reporter.
27
+ *
28
+ * The reporter is a paired add-on for `@koderlabs/tasks-sdk-rn` — pass either
29
+ * the bare core `Client` or the `RnClientHandle` returned by `init()`.
30
+ */
31
+ interface InitReporterOptions {
32
+ /**
33
+ * Ref to the root `<View>` that should be captured into a screenshot.
34
+ * Required for screenshot capture; if omitted, reports submit text-only.
35
+ *
36
+ * The capture is taken *before* the modal is presented so the modal
37
+ * never appears in the screenshot.
38
+ */
39
+ captureRef?: RefObject<unknown>;
40
+ /**
41
+ * Listen for device shake gestures and auto-open the reporter on shake.
42
+ * iOS: uses the built-in shake event. Android: requires `expo-sensors`
43
+ * to be installed (graceful no-op otherwise).
44
+ * Default: false.
45
+ */
46
+ enableShakeGesture?: boolean;
47
+ /**
48
+ * On iOS, show an `Alert.alert("Send screenshot?")` prompt whenever the
49
+ * user takes a system screenshot. Requires `expo-screen-capture`.
50
+ * Android: no-op (OS does not expose a reliable hook).
51
+ * Default: false.
52
+ */
53
+ enableScreenshotDetection?: boolean;
54
+ /** Default issue type id applied to created tickets. */
55
+ defaultIssueTypeId?: string;
56
+ /** Default priority id applied to created tickets. */
57
+ defaultPriorityId?: string;
58
+ /**
59
+ * Show an email field in the modal so anonymous reporters can identify
60
+ * themselves. If your app already authenticates users you can leave this
61
+ * off and pass the user's email via `client` user context elsewhere.
62
+ * Default: false.
63
+ */
64
+ collectEmail?: boolean;
65
+ /** Suppress success/error alerts. Default: false (alerts shown). */
66
+ silent?: boolean;
67
+ /** Extra custom data merged into every report's payload. */
68
+ customData?: Record<string, unknown>;
69
+ }
70
+ /** Imperative handle returned by `initReporter()`. */
71
+ interface ReporterHandle {
72
+ /** Capture a screenshot (if captureRef provided) then open the modal. */
73
+ show(): void;
74
+ /** Hide the modal without sending. Alias for close(). */
75
+ hide(): void;
76
+ /** Tear down listeners (shake, screenshot-detection) and hide modal. */
77
+ close(): void;
78
+ }
79
+ /** Payload submitted by the modal — internal. */
80
+ interface BugReportSubmission {
81
+ description: string;
82
+ email?: string;
83
+ screenshotUri?: string;
84
+ /** Annotator draw commands captured over the screenshot. v1: shipped raw; server-side bake pending. */
85
+ annotations?: DrawCommand[];
86
+ defaultIssueTypeId?: string;
87
+ defaultPriorityId?: string;
88
+ customData?: Record<string, unknown>;
89
+ }
90
+ /** Result returned from a successful submission. */
91
+ interface BugReportResult {
92
+ ticketKey: string;
93
+ ticketUrl: string;
94
+ }
95
+
96
+ /**
97
+ * Reporter transport — wraps `client.send()` for `kind: 'bug_report'` events.
98
+ *
99
+ * The core SDK already routes events that carry attachments through the
100
+ * `/sdk/events/multipart` ingest endpoint, so all we have to do is build the
101
+ * event payload + (optional) screenshot Blob and call `client.send()`.
102
+ */
103
+
104
+ /** Either the bare core Client or an `RnClientHandle` from sdk-rn. */
105
+ type ReporterClient = Client | {
106
+ client: Client;
107
+ };
108
+ /**
109
+ * Build the bug_report event + attachments map and submit via client.send().
110
+ */
111
+ declare function submitBugReport(c: ReporterClient, submission: BugReportSubmission): Promise<BugReportResult>;
112
+
113
+ /**
114
+ * RN Annotator — box highlight + redact tool overlay for the captured screenshot.
115
+ *
116
+ * Mirrors the web-reporter `DrawCommand` shape (normalized 0..1 coords against
117
+ * the captured image dimensions) so the backend can render baked annotations
118
+ * uniformly across platforms.
119
+ *
120
+ * v1: emits `DrawCommand[]` only — the transport ships them alongside the raw
121
+ * screenshot. Server-side compositing is deferred (see README).
122
+ *
123
+ * Uses `react-native-svg` (peer dep, optional) for vector overlay rendering
124
+ * and RN's built-in `PanResponder` for tap-drag gestures — no extra gesture
125
+ * library required.
126
+ */
127
+
128
+ type AnnotatorTool = 'highlight' | 'hide';
129
+ interface AnnotatorProps {
130
+ /** file:// URI of the captured screenshot. */
131
+ imageUri: string;
132
+ /** Called when user taps "Done" — receives the list of normalized commands. */
133
+ onCommit: (commands: DrawCommand[]) => void;
134
+ /** Called when user taps "Cancel". */
135
+ onCancel: () => void;
136
+ }
137
+ /**
138
+ * The annotator renders the captured screenshot full-screen with a transparent
139
+ * SVG overlay that captures pan gestures and translates them into committed
140
+ * `DrawCommand`s. Tool palette lives at the bottom with Undo / Clear / Done.
141
+ */
142
+ declare function Annotator(props: AnnotatorProps): React.ReactElement | null;
143
+
144
+ /**
145
+ * @koderlabs/tasks-sdk-rn-reporter
146
+ *
147
+ * In-app bug reporter for React Native — pair with `@koderlabs/tasks-sdk-rn`.
148
+ *
149
+ * Usage:
150
+ * import { init } from '@koderlabs/tasks-sdk-rn';
151
+ * import { initReporter } from '@koderlabs/tasks-sdk-rn-reporter';
152
+ *
153
+ * const rootRef = useRef<View>(null);
154
+ * const sdk = init({ projectId: 'FE', accessKey: 'sk_…' });
155
+ * const reporter = initReporter(sdk, {
156
+ * captureRef: rootRef,
157
+ * enableShakeGesture: true,
158
+ * });
159
+ * // reporter.show() — open modal manually
160
+ */
161
+
162
+ /**
163
+ * Initialise the bug reporter.
164
+ *
165
+ * Returns a handle whose `show()` you can wire to a button (FAB, menu item,
166
+ * keyboard shortcut, etc.) and whose `close()` you should call on unmount.
167
+ *
168
+ * The reporter renders its own `<Modal>` via React Native's portal-like
169
+ * built-in Modal component, so callers do NOT need to mount any extra
170
+ * component in their tree.
171
+ */
172
+ declare function initReporter(client: ReporterClient, opts?: InitReporterOptions): ReporterHandle;
173
+ declare function ReporterRoot(): React.ReactElement | null;
174
+
175
+ export { Annotator, type AnnotatorProps, type AnnotatorTool, type BugReportResult, type DrawCommand, type InitReporterOptions, type ReporterHandle, ReporterRoot, initReporter as default, initReporter, submitBugReport };