@bbearai/react-native 0.3.10 → 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.
package/dist/index.d.mts CHANGED
@@ -1,5 +1,6 @@
1
- import React, { ReactNode } from 'react';
2
- import { BugBearConfig, BugBearClient, TesterInfo, TestAssignment, DeviceInfo, TesterThread, TesterMessage, QASession, QAFinding, StartSessionOptions, AddFindingOptions, TesterProfileUpdate, AppContext } from '@bbearai/core';
1
+ import React, { ReactNode, Component, ErrorInfo } from 'react';
2
+ import * as _bbearai_core from '@bbearai/core';
3
+ import { BugBearConfig, BugBearClient, TesterInfo, TestAssignment, DeviceInfo, TesterThread, TesterMessage, QASession, QAFinding, StartSessionOptions, AddFindingOptions, TesterProfileUpdate, AppContext, captureError } from '@bbearai/core';
3
4
  export { AppContext, BugBearConfig, BugBearReport, DeviceInfo, MessageSenderType, ReportType, Severity, TestAssignment, TesterInfo, TesterMessage, TesterThread, ThreadPriority, ThreadType } from '@bbearai/core';
4
5
 
5
6
  interface BugBearContextValue {
@@ -74,6 +75,8 @@ interface BugBearContextValue {
74
75
  refreshTesterInfo: () => Promise<void>;
75
76
  /** URL to the BugBear web dashboard (for linking testers to the full web experience) */
76
77
  dashboardUrl?: string;
78
+ /** Error handler from config — wire to your error reporting service */
79
+ onError?: (error: Error, context?: Record<string, unknown>) => void;
77
80
  }
78
81
  declare function useBugBear(): BugBearContextValue;
79
82
  interface BugBearProviderProps {
@@ -110,4 +113,36 @@ interface BugBearButtonProps {
110
113
  }
111
114
  declare function BugBearButton({ position, buttonStyle, draggable, initialX, initialY, minY, maxYOffset, }: BugBearButtonProps): React.JSX.Element | null;
112
115
 
113
- export { BugBearButton, BugBearProvider, useBugBear };
116
+ interface Props {
117
+ children: ReactNode;
118
+ /** Fallback UI to show when an error occurs */
119
+ fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode);
120
+ /** Called when an error is captured (React ErrorInfo included) */
121
+ onError?: (error: Error, errorInfo: ErrorInfo) => void;
122
+ /** Error reporter from BugBearConfig — wire to Sentry.captureException etc. */
123
+ errorReporter?: (error: Error, context?: Record<string, unknown>) => void;
124
+ }
125
+ interface State {
126
+ hasError: boolean;
127
+ error: Error | null;
128
+ errorInfo: ErrorInfo | null;
129
+ }
130
+ /**
131
+ * Error boundary that captures React Native errors and integrates with BugBear
132
+ */
133
+ declare class BugBearErrorBoundary extends Component<Props, State> {
134
+ constructor(props: Props);
135
+ static getDerivedStateFromError(error: Error): Partial<State>;
136
+ componentDidCatch(error: Error, errorInfo: ErrorInfo): void;
137
+ reset: () => void;
138
+ render(): ReactNode;
139
+ }
140
+ /**
141
+ * Hook to get current error context for manual error reporting
142
+ */
143
+ declare function useErrorContext(): {
144
+ captureError: typeof captureError;
145
+ getEnhancedContext: () => _bbearai_core.EnhancedBugContext;
146
+ };
147
+
148
+ export { BugBearButton, BugBearErrorBoundary, BugBearProvider, useBugBear, useErrorContext };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import React, { ReactNode } from 'react';
2
- import { BugBearConfig, BugBearClient, TesterInfo, TestAssignment, DeviceInfo, TesterThread, TesterMessage, QASession, QAFinding, StartSessionOptions, AddFindingOptions, TesterProfileUpdate, AppContext } from '@bbearai/core';
1
+ import React, { ReactNode, Component, ErrorInfo } from 'react';
2
+ import * as _bbearai_core from '@bbearai/core';
3
+ import { BugBearConfig, BugBearClient, TesterInfo, TestAssignment, DeviceInfo, TesterThread, TesterMessage, QASession, QAFinding, StartSessionOptions, AddFindingOptions, TesterProfileUpdate, AppContext, captureError } from '@bbearai/core';
3
4
  export { AppContext, BugBearConfig, BugBearReport, DeviceInfo, MessageSenderType, ReportType, Severity, TestAssignment, TesterInfo, TesterMessage, TesterThread, ThreadPriority, ThreadType } from '@bbearai/core';
4
5
 
5
6
  interface BugBearContextValue {
@@ -74,6 +75,8 @@ interface BugBearContextValue {
74
75
  refreshTesterInfo: () => Promise<void>;
75
76
  /** URL to the BugBear web dashboard (for linking testers to the full web experience) */
76
77
  dashboardUrl?: string;
78
+ /** Error handler from config — wire to your error reporting service */
79
+ onError?: (error: Error, context?: Record<string, unknown>) => void;
77
80
  }
78
81
  declare function useBugBear(): BugBearContextValue;
79
82
  interface BugBearProviderProps {
@@ -110,4 +113,36 @@ interface BugBearButtonProps {
110
113
  }
111
114
  declare function BugBearButton({ position, buttonStyle, draggable, initialX, initialY, minY, maxYOffset, }: BugBearButtonProps): React.JSX.Element | null;
112
115
 
113
- export { BugBearButton, BugBearProvider, useBugBear };
116
+ interface Props {
117
+ children: ReactNode;
118
+ /** Fallback UI to show when an error occurs */
119
+ fallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode);
120
+ /** Called when an error is captured (React ErrorInfo included) */
121
+ onError?: (error: Error, errorInfo: ErrorInfo) => void;
122
+ /** Error reporter from BugBearConfig — wire to Sentry.captureException etc. */
123
+ errorReporter?: (error: Error, context?: Record<string, unknown>) => void;
124
+ }
125
+ interface State {
126
+ hasError: boolean;
127
+ error: Error | null;
128
+ errorInfo: ErrorInfo | null;
129
+ }
130
+ /**
131
+ * Error boundary that captures React Native errors and integrates with BugBear
132
+ */
133
+ declare class BugBearErrorBoundary extends Component<Props, State> {
134
+ constructor(props: Props);
135
+ static getDerivedStateFromError(error: Error): Partial<State>;
136
+ componentDidCatch(error: Error, errorInfo: ErrorInfo): void;
137
+ reset: () => void;
138
+ render(): ReactNode;
139
+ }
140
+ /**
141
+ * Hook to get current error context for manual error reporting
142
+ */
143
+ declare function useErrorContext(): {
144
+ captureError: typeof captureError;
145
+ getEnhancedContext: () => _bbearai_core.EnhancedBugContext;
146
+ };
147
+
148
+ export { BugBearButton, BugBearErrorBoundary, BugBearProvider, useBugBear, useErrorContext };
package/dist/index.js CHANGED
@@ -31,8 +31,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
33
  BugBearButton: () => BugBearButton,
34
+ BugBearErrorBoundary: () => BugBearErrorBoundary,
34
35
  BugBearProvider: () => BugBearProvider,
35
- useBugBear: () => useBugBear
36
+ useBugBear: () => useBugBear,
37
+ useErrorContext: () => useErrorContext
36
38
  });
37
39
  module.exports = __toCommonJS(index_exports);
38
40
 
@@ -11534,7 +11536,7 @@ var ContextCaptureManager = class {
11534
11536
  });
11535
11537
  }
11536
11538
  captureFetch() {
11537
- if (typeof window === "undefined" || typeof fetch === "undefined") return;
11539
+ if (typeof window === "undefined" || typeof fetch === "undefined" || typeof document === "undefined") return;
11538
11540
  this.originalFetch = window.fetch;
11539
11541
  const self2 = this;
11540
11542
  window.fetch = async function(input, init) {
@@ -11625,6 +11627,13 @@ var ContextCaptureManager = class {
11625
11627
  }
11626
11628
  };
11627
11629
  var contextCapture = new ContextCaptureManager();
11630
+ function captureError(error, errorInfo) {
11631
+ return {
11632
+ errorMessage: error.message,
11633
+ errorStack: error.stack,
11634
+ componentStack: errorInfo?.componentStack
11635
+ };
11636
+ }
11628
11637
  var DEFAULT_SUPABASE_URL = "https://kyxgzjnqgvapvlnvqawz.supabase.co";
11629
11638
  var getEnvVar = (key) => {
11630
11639
  try {
@@ -11792,7 +11801,7 @@ var BugBearClient = class {
11792
11801
  sort_order
11793
11802
  )
11794
11803
  )
11795
- `).eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).in("status", ["pending", "in_progress"]).order("created_at", { ascending: true });
11804
+ `).eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).in("status", ["pending", "in_progress"]).order("created_at", { ascending: true }).limit(100);
11796
11805
  if (error) {
11797
11806
  console.error("BugBear: Failed to fetch assignments", error);
11798
11807
  return [];
@@ -11946,7 +11955,7 @@ var BugBearClient = class {
11946
11955
  if (options?.testResult) {
11947
11956
  updateData.test_result = options.testResult;
11948
11957
  }
11949
- const { error } = await this.supabase.from("test_assignments").update(updateData).eq("id", assignmentId);
11958
+ const { data: updatedRow, error } = await this.supabase.from("test_assignments").update(updateData).eq("id", assignmentId).eq("status", currentAssignment.status).select("id").maybeSingle();
11950
11959
  if (error) {
11951
11960
  console.error("BugBear: Failed to update assignment status", {
11952
11961
  message: error.message,
@@ -11959,6 +11968,9 @@ var BugBearClient = class {
11959
11968
  });
11960
11969
  return { success: false, error: error.message };
11961
11970
  }
11971
+ if (!updatedRow) {
11972
+ return { success: false, error: "Assignment status has changed. Please refresh and try again." };
11973
+ }
11962
11974
  if (options?.feedback && ["passed", "failed", "blocked"].includes(status)) {
11963
11975
  const { data: assignmentData, error: fetchError2 } = await this.supabase.from("test_assignments").select("test_case_id").eq("id", assignmentId).single();
11964
11976
  if (fetchError2) {
@@ -12185,6 +12197,28 @@ var BugBearClient = class {
12185
12197
  return null;
12186
12198
  }
12187
12199
  }
12200
+ /**
12201
+ * Get detailed assignment stats for the current tester via RPC.
12202
+ * Returns counts by status (pending, in_progress, passed, failed, blocked, skipped, total).
12203
+ */
12204
+ async getTesterStats() {
12205
+ try {
12206
+ const testerInfo = await this.getTesterInfo();
12207
+ if (!testerInfo) return null;
12208
+ const { data, error } = await this.supabase.rpc("get_tester_stats", {
12209
+ p_project_id: this.config.projectId,
12210
+ p_tester_id: testerInfo.id
12211
+ });
12212
+ if (error) {
12213
+ console.error("BugBear: Failed to fetch tester stats", error);
12214
+ return null;
12215
+ }
12216
+ return data;
12217
+ } catch (err) {
12218
+ console.error("BugBear: Error fetching tester stats", err);
12219
+ return null;
12220
+ }
12221
+ }
12188
12222
  /**
12189
12223
  * Basic email format validation (defense in depth)
12190
12224
  */
@@ -12518,75 +12552,35 @@ var BugBearClient = class {
12518
12552
  try {
12519
12553
  const testerInfo = await this.getTesterInfo();
12520
12554
  if (!testerInfo) return [];
12521
- const { data: threads, error } = await this.supabase.from("discussion_threads").select(`
12522
- id,
12523
- subject,
12524
- thread_type,
12525
- priority,
12526
- is_pinned,
12527
- is_resolved,
12528
- last_message_at,
12529
- created_at
12530
- `).eq("project_id", this.config.projectId).or(`audience.eq.all,audience_tester_ids.cs.{${testerInfo.id}}`).order("is_pinned", { ascending: false }).order("last_message_at", { ascending: false });
12555
+ const { data, error } = await this.supabase.rpc("get_threads_with_unread", {
12556
+ p_project_id: this.config.projectId,
12557
+ p_tester_id: testerInfo.id
12558
+ });
12531
12559
  if (error) {
12532
- console.error("BugBear: Failed to fetch threads", error);
12560
+ console.error("BugBear: Failed to fetch threads via RPC", error);
12533
12561
  return [];
12534
12562
  }
12535
- if (!threads || threads.length === 0) return [];
12536
- const threadIds = threads.map((t) => t.id);
12537
- const { data: readStatuses } = await this.supabase.from("discussion_read_status").select("thread_id, last_read_at, last_read_message_id").eq("tester_id", testerInfo.id).in("thread_id", threadIds);
12538
- const readStatusMap = new Map(
12539
- (readStatuses || []).map((rs) => [rs.thread_id, rs])
12540
- );
12541
- const { data: lastMessages } = await this.supabase.from("discussion_messages").select(`
12542
- id,
12543
- thread_id,
12544
- sender_type,
12545
- sender_name,
12546
- content,
12547
- created_at,
12548
- attachments
12549
- `).in("thread_id", threadIds).order("created_at", { ascending: false });
12550
- const lastMessageMap = /* @__PURE__ */ new Map();
12551
- for (const msg of lastMessages || []) {
12552
- if (!lastMessageMap.has(msg.thread_id)) {
12553
- lastMessageMap.set(msg.thread_id, msg);
12554
- }
12555
- }
12556
- const unreadCounts = await Promise.all(
12557
- threads.map(async (thread) => {
12558
- const readStatus = readStatusMap.get(thread.id);
12559
- const lastReadAt = readStatus?.last_read_at || "1970-01-01T00:00:00Z";
12560
- const { count, error: countError } = await this.supabase.from("discussion_messages").select("*", { count: "exact", head: true }).eq("thread_id", thread.id).gt("created_at", lastReadAt);
12561
- return { threadId: thread.id, count: countError ? 0 : count || 0 };
12562
- })
12563
- );
12564
- const unreadCountMap = new Map(
12565
- unreadCounts.map((uc) => [uc.threadId, uc.count])
12566
- );
12567
- return threads.map((thread) => {
12568
- const lastMsg = lastMessageMap.get(thread.id);
12569
- return {
12570
- id: thread.id,
12571
- subject: thread.subject,
12572
- threadType: thread.thread_type,
12573
- priority: thread.priority,
12574
- isPinned: thread.is_pinned,
12575
- isResolved: thread.is_resolved,
12576
- lastMessageAt: thread.last_message_at,
12577
- createdAt: thread.created_at,
12578
- unreadCount: unreadCountMap.get(thread.id) || 0,
12579
- lastMessage: lastMsg ? {
12580
- id: lastMsg.id,
12581
- threadId: lastMsg.thread_id,
12582
- senderType: lastMsg.sender_type,
12583
- senderName: lastMsg.sender_name,
12584
- content: lastMsg.content,
12585
- createdAt: lastMsg.created_at,
12586
- attachments: lastMsg.attachments || []
12587
- } : void 0
12588
- };
12589
- });
12563
+ if (!data || data.length === 0) return [];
12564
+ return data.map((row) => ({
12565
+ id: row.thread_id,
12566
+ subject: row.thread_subject,
12567
+ threadType: row.thread_type,
12568
+ priority: row.thread_priority,
12569
+ isPinned: row.is_pinned,
12570
+ isResolved: row.is_resolved,
12571
+ lastMessageAt: row.last_message_at,
12572
+ createdAt: row.created_at,
12573
+ unreadCount: Number(row.unread_count) || 0,
12574
+ lastMessage: row.last_message_preview ? {
12575
+ id: "",
12576
+ threadId: row.thread_id,
12577
+ senderType: row.last_message_sender_type || "system",
12578
+ senderName: row.last_message_sender_name || "",
12579
+ content: row.last_message_preview,
12580
+ createdAt: row.last_message_at,
12581
+ attachments: []
12582
+ } : void 0
12583
+ }));
12590
12584
  } catch (err) {
12591
12585
  console.error("BugBear: Error fetching threads", err);
12592
12586
  return [];
@@ -12605,7 +12599,7 @@ var BugBearClient = class {
12605
12599
  content,
12606
12600
  created_at,
12607
12601
  attachments
12608
- `).eq("thread_id", threadId).order("created_at", { ascending: true });
12602
+ `).eq("thread_id", threadId).order("created_at", { ascending: true }).limit(200);
12609
12603
  if (error) {
12610
12604
  console.error("BugBear: Failed to fetch messages", error);
12611
12605
  return [];
@@ -12654,7 +12648,6 @@ var BugBearClient = class {
12654
12648
  console.error("BugBear: Failed to send message", error);
12655
12649
  return false;
12656
12650
  }
12657
- await this.supabase.from("discussion_threads").update({ last_message_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", threadId);
12658
12651
  await this.markThreadAsRead(threadId);
12659
12652
  return true;
12660
12653
  } catch (err) {
@@ -12885,7 +12878,7 @@ var BugBearClient = class {
12885
12878
  */
12886
12879
  async getSessionFindings(sessionId) {
12887
12880
  try {
12888
- const { data, error } = await this.supabase.from("qa_findings").select("*").eq("session_id", sessionId).order("created_at", { ascending: true });
12881
+ const { data, error } = await this.supabase.from("qa_findings").select("*").eq("session_id", sessionId).order("created_at", { ascending: true }).limit(100);
12889
12882
  if (error) {
12890
12883
  console.error("BugBear: Failed to fetch findings", error);
12891
12884
  return [];
@@ -13029,7 +13022,8 @@ var BugBearContext = (0, import_react.createContext)({
13029
13022
  updateTesterProfile: async () => ({ success: false }),
13030
13023
  refreshTesterInfo: async () => {
13031
13024
  },
13032
- dashboardUrl: void 0
13025
+ dashboardUrl: void 0,
13026
+ onError: void 0
13033
13027
  });
13034
13028
  function useBugBear() {
13035
13029
  return (0, import_react.useContext)(BugBearContext);
@@ -13178,6 +13172,9 @@ function BugBearProvider({ config, children, appVersion, enabled = true }) {
13178
13172
  }
13179
13173
  } catch (err) {
13180
13174
  console.error("BugBear: Init error", err);
13175
+ if (err instanceof Error) {
13176
+ config.onError?.(err, { phase: "init" });
13177
+ }
13181
13178
  } finally {
13182
13179
  setIsLoading(false);
13183
13180
  }
@@ -13235,7 +13232,8 @@ function BugBearProvider({ config, children, appVersion, enabled = true }) {
13235
13232
  refreshTesterStatus,
13236
13233
  updateTesterProfile,
13237
13234
  refreshTesterInfo,
13238
- dashboardUrl: config.dashboardUrl
13235
+ dashboardUrl: config.dashboardUrl,
13236
+ onError: config.onError
13239
13237
  }
13240
13238
  },
13241
13239
  children
@@ -13745,6 +13743,7 @@ function TestDetailScreen({ testId, nav }) {
13745
13743
  const [selectedSkipReason, setSelectedSkipReason] = (0, import_react4.useState)(null);
13746
13744
  const [skipNotes, setSkipNotes] = (0, import_react4.useState)("");
13747
13745
  const [skipping, setSkipping] = (0, import_react4.useState)(false);
13746
+ const [isSubmitting, setIsSubmitting] = (0, import_react4.useState)(false);
13748
13747
  (0, import_react4.useEffect)(() => {
13749
13748
  setCriteriaResults({});
13750
13749
  setShowSteps(true);
@@ -13767,26 +13766,36 @@ function TestDetailScreen({ testId, nav }) {
13767
13766
  const allTests = assignments;
13768
13767
  const currentIndex = displayedAssignment ? assignments.indexOf(displayedAssignment) : -1;
13769
13768
  const handlePass = (0, import_react4.useCallback)(async () => {
13770
- if (!client || !displayedAssignment) return;
13769
+ if (!client || !displayedAssignment || isSubmitting) return;
13771
13770
  import_react_native4.Keyboard.dismiss();
13772
- await client.passAssignment(displayedAssignment.id);
13773
- await refreshAssignments();
13774
- nav.replace({ name: "TEST_FEEDBACK", status: "passed", assignmentId: displayedAssignment.id });
13775
- }, [client, displayedAssignment, refreshAssignments, nav]);
13771
+ setIsSubmitting(true);
13772
+ try {
13773
+ await client.passAssignment(displayedAssignment.id);
13774
+ await refreshAssignments();
13775
+ nav.replace({ name: "TEST_FEEDBACK", status: "passed", assignmentId: displayedAssignment.id });
13776
+ } finally {
13777
+ setIsSubmitting(false);
13778
+ }
13779
+ }, [client, displayedAssignment, refreshAssignments, nav, isSubmitting]);
13776
13780
  const handleFail = (0, import_react4.useCallback)(async () => {
13777
- if (!client || !displayedAssignment) return;
13781
+ if (!client || !displayedAssignment || isSubmitting) return;
13778
13782
  import_react_native4.Keyboard.dismiss();
13779
- await client.failAssignment(displayedAssignment.id);
13780
- await refreshAssignments();
13781
- nav.replace({
13782
- name: "REPORT",
13783
- prefill: {
13784
- type: "test_fail",
13785
- assignmentId: displayedAssignment.id,
13786
- testCaseId: displayedAssignment.testCase.id
13787
- }
13788
- });
13789
- }, [client, displayedAssignment, refreshAssignments, nav]);
13783
+ setIsSubmitting(true);
13784
+ try {
13785
+ await client.failAssignment(displayedAssignment.id);
13786
+ await refreshAssignments();
13787
+ nav.replace({
13788
+ name: "REPORT",
13789
+ prefill: {
13790
+ type: "test_fail",
13791
+ assignmentId: displayedAssignment.id,
13792
+ testCaseId: displayedAssignment.testCase.id
13793
+ }
13794
+ });
13795
+ } finally {
13796
+ setIsSubmitting(false);
13797
+ }
13798
+ }, [client, displayedAssignment, refreshAssignments, nav, isSubmitting]);
13790
13799
  const handleSkip = (0, import_react4.useCallback)(async () => {
13791
13800
  if (!client || !displayedAssignment || !selectedSkipReason) return;
13792
13801
  import_react_native4.Keyboard.dismiss();
@@ -13867,7 +13876,7 @@ function TestDetailScreen({ testId, nav }) {
13867
13876
  }
13868
13877
  },
13869
13878
  /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Text, { style: styles2.navigateText }, "\u{1F9ED} Go to test location")
13870
- ), /* @__PURE__ */ import_react4.default.createElement(import_react_native4.TouchableOpacity, { onPress: () => setShowDetails(!showDetails), style: styles2.detailsToggle }, /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Text, { style: styles2.detailsToggleText }, showDetails ? "\u25BC" : "\u25B6", " Details")), showDetails && /* @__PURE__ */ import_react4.default.createElement(import_react_native4.View, { style: styles2.detailsSection }, testCase.key && /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Text, { style: styles2.detailMeta }, testCase.key, " \xB7 ", testCase.priority, " \xB7 ", info.name), testCase.description && /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Text, { style: styles2.detailDesc }, testCase.description), testCase.group && /* @__PURE__ */ import_react4.default.createElement(import_react_native4.View, { style: styles2.folderProgress }, /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Text, { style: styles2.folderName }, "\u{1F4C1} ", testCase.group.name))), /* @__PURE__ */ import_react4.default.createElement(import_react_native4.View, { style: styles2.actionButtons }, /* @__PURE__ */ import_react4.default.createElement(import_react_native4.TouchableOpacity, { style: [styles2.actionBtn, styles2.failBtn], onPress: handleFail }, /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Text, { style: styles2.failBtnText }, "Fail")), /* @__PURE__ */ import_react4.default.createElement(import_react_native4.TouchableOpacity, { style: [styles2.actionBtn, styles2.skipBtn], onPress: () => setShowSkipModal(true) }, /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Text, { style: styles2.skipBtnText }, "Skip")), /* @__PURE__ */ import_react4.default.createElement(import_react_native4.TouchableOpacity, { style: [styles2.actionBtn, styles2.passBtn], onPress: handlePass }, /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Text, { style: styles2.passBtnText }, "Pass"))), /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Modal, { visible: showSkipModal, transparent: true, animationType: "fade" }, /* @__PURE__ */ import_react4.default.createElement(import_react_native4.View, { style: styles2.modalOverlay }, /* @__PURE__ */ import_react4.default.createElement(import_react_native4.View, { style: styles2.modalContent }, /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Text, { style: styles2.modalTitle }, "Skip this test?"), /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Text, { style: styles2.modalSubtitle }, "Select a reason:"), [
13879
+ ), /* @__PURE__ */ import_react4.default.createElement(import_react_native4.TouchableOpacity, { onPress: () => setShowDetails(!showDetails), style: styles2.detailsToggle }, /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Text, { style: styles2.detailsToggleText }, showDetails ? "\u25BC" : "\u25B6", " Details")), showDetails && /* @__PURE__ */ import_react4.default.createElement(import_react_native4.View, { style: styles2.detailsSection }, testCase.key && /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Text, { style: styles2.detailMeta }, testCase.key, " \xB7 ", testCase.priority, " \xB7 ", info.name), testCase.description && /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Text, { style: styles2.detailDesc }, testCase.description), testCase.group && /* @__PURE__ */ import_react4.default.createElement(import_react_native4.View, { style: styles2.folderProgress }, /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Text, { style: styles2.folderName }, "\u{1F4C1} ", testCase.group.name))), /* @__PURE__ */ import_react4.default.createElement(import_react_native4.View, { style: styles2.actionButtons }, /* @__PURE__ */ import_react4.default.createElement(import_react_native4.TouchableOpacity, { style: [styles2.actionBtn, styles2.failBtn, isSubmitting && { opacity: 0.5 }], onPress: handleFail, disabled: isSubmitting }, /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Text, { style: styles2.failBtnText }, isSubmitting ? "Failing..." : "Fail")), /* @__PURE__ */ import_react4.default.createElement(import_react_native4.TouchableOpacity, { style: [styles2.actionBtn, styles2.skipBtn, isSubmitting && { opacity: 0.5 }], onPress: () => setShowSkipModal(true), disabled: isSubmitting }, /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Text, { style: styles2.skipBtnText }, "Skip")), /* @__PURE__ */ import_react4.default.createElement(import_react_native4.TouchableOpacity, { style: [styles2.actionBtn, styles2.passBtn, isSubmitting && { opacity: 0.5 }], onPress: handlePass, disabled: isSubmitting }, /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Text, { style: styles2.passBtnText }, isSubmitting ? "Passing..." : "Pass"))), /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Modal, { visible: showSkipModal, transparent: true, animationType: "fade" }, /* @__PURE__ */ import_react4.default.createElement(import_react_native4.View, { style: styles2.modalOverlay }, /* @__PURE__ */ import_react4.default.createElement(import_react_native4.View, { style: styles2.modalContent }, /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Text, { style: styles2.modalTitle }, "Skip this test?"), /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Text, { style: styles2.modalSubtitle }, "Select a reason:"), [
13871
13880
  { reason: "blocked", label: "\u{1F6AB} Blocked by a bug" },
13872
13881
  { reason: "not_ready", label: "\u{1F6A7} Feature not ready" },
13873
13882
  { reason: "dependency", label: "\u{1F517} Needs another test first" },
@@ -14435,32 +14444,46 @@ function ReportScreen({ nav, prefill }) {
14435
14444
  const [description, setDescription] = (0, import_react10.useState)("");
14436
14445
  const [affectedScreen, setAffectedScreen] = (0, import_react10.useState)("");
14437
14446
  const [submitting, setSubmitting] = (0, import_react10.useState)(false);
14447
+ const [error, setError] = (0, import_react10.useState)(null);
14438
14448
  const images = useImageAttachments(uploadImage, 5, "screenshots");
14439
14449
  const isBugType = reportType === "bug" || reportType === "test_fail";
14440
14450
  const handleSubmit = async () => {
14441
14451
  if (!client || !description.trim()) return;
14442
14452
  setSubmitting(true);
14443
- const baseContext = client.getAppContext();
14444
- const appContext = {
14445
- ...baseContext,
14446
- currentRoute: affectedScreen.trim() || baseContext.currentRoute
14447
- };
14448
- const screenshotUrls = images.getScreenshotUrls();
14449
- await client.submitReport({
14450
- type: reportType,
14451
- description: description.trim(),
14452
- severity: isBugType ? severity : void 0,
14453
- assignmentId: prefill?.assignmentId,
14454
- testCaseId: prefill?.testCaseId,
14455
- appContext,
14456
- deviceInfo: getDeviceInfo(),
14457
- screenshots: screenshotUrls.length > 0 ? screenshotUrls : void 0
14458
- });
14459
- if (prefill?.assignmentId) {
14460
- await refreshAssignments();
14453
+ setError(null);
14454
+ try {
14455
+ const baseContext = client.getAppContext();
14456
+ const appContext = {
14457
+ ...baseContext,
14458
+ currentRoute: affectedScreen.trim() || baseContext.currentRoute
14459
+ };
14460
+ const screenshotUrls = images.getScreenshotUrls();
14461
+ const result = await client.submitReport({
14462
+ type: reportType,
14463
+ description: description.trim(),
14464
+ severity: isBugType ? severity : void 0,
14465
+ assignmentId: prefill?.assignmentId,
14466
+ testCaseId: prefill?.testCaseId,
14467
+ appContext,
14468
+ deviceInfo: getDeviceInfo(),
14469
+ screenshots: screenshotUrls.length > 0 ? screenshotUrls : void 0
14470
+ });
14471
+ if (!result.success) {
14472
+ console.error("BugBear: Report submission failed", result.error);
14473
+ setError(result.error || "Failed to submit report. Please try again.");
14474
+ setSubmitting(false);
14475
+ return;
14476
+ }
14477
+ if (prefill?.assignmentId) {
14478
+ await refreshAssignments();
14479
+ }
14480
+ setSubmitting(false);
14481
+ nav.replace({ name: "REPORT_SUCCESS" });
14482
+ } catch (err) {
14483
+ console.error("BugBear: Report submission error", err);
14484
+ setError(err instanceof Error ? err.message : "An unexpected error occurred. Please try again.");
14485
+ setSubmitting(false);
14461
14486
  }
14462
- setSubmitting(false);
14463
- nav.replace({ name: "REPORT_SUCCESS" });
14464
14487
  };
14465
14488
  return /* @__PURE__ */ import_react10.default.createElement(import_react_native9.View, null, /* @__PURE__ */ import_react10.default.createElement(import_react_native9.Text, { style: shared.label }, "What are you reporting?"), /* @__PURE__ */ import_react10.default.createElement(import_react_native9.View, { style: styles7.typeRow }, [
14466
14489
  { type: "bug", label: "Bug", icon: "\u{1F41B}" },
@@ -14519,14 +14542,14 @@ function ReportScreen({ nav, prefill }) {
14519
14542
  onRemove: images.removeImage,
14520
14543
  label: "Screenshots (optional)"
14521
14544
  }
14522
- ), /* @__PURE__ */ import_react10.default.createElement(
14545
+ ), error && /* @__PURE__ */ import_react10.default.createElement(import_react_native9.View, { style: styles7.errorBanner }, /* @__PURE__ */ import_react10.default.createElement(import_react_native9.Text, { style: styles7.errorText }, error)), /* @__PURE__ */ import_react10.default.createElement(
14523
14546
  import_react_native9.TouchableOpacity,
14524
14547
  {
14525
14548
  style: [shared.primaryButton, (!description.trim() || submitting || images.isUploading) && shared.primaryButtonDisabled, { marginTop: 20 }],
14526
14549
  onPress: handleSubmit,
14527
14550
  disabled: !description.trim() || submitting || images.isUploading
14528
14551
  },
14529
- /* @__PURE__ */ import_react10.default.createElement(import_react_native9.Text, { style: shared.primaryButtonText }, images.isUploading ? "Uploading images..." : submitting ? "Submitting..." : "Submit Report")
14552
+ /* @__PURE__ */ import_react10.default.createElement(import_react_native9.Text, { style: shared.primaryButtonText }, images.isUploading ? "Uploading images..." : submitting ? "Submitting..." : error ? "Retry" : "Submit Report")
14530
14553
  ));
14531
14554
  }
14532
14555
  var styles7 = import_react_native9.StyleSheet.create({
@@ -14542,7 +14565,9 @@ var styles7 = import_react_native9.StyleSheet.create({
14542
14565
  sevText: { fontSize: 12, fontWeight: "500", color: colors.textSecondary, textTransform: "capitalize" },
14543
14566
  descInput: { backgroundColor: colors.card, borderWidth: 1, borderColor: colors.border, borderRadius: 12, paddingHorizontal: 14, paddingVertical: 12, fontSize: 14, color: colors.textPrimary, minHeight: 100 },
14544
14567
  screenInput: { backgroundColor: colors.card, borderWidth: 1, borderColor: colors.border, borderRadius: 8, paddingHorizontal: 12, paddingVertical: 8, fontSize: 13, color: colors.textPrimary },
14545
- screenHint: { fontSize: 11, color: colors.textMuted, marginTop: 4 }
14568
+ screenHint: { fontSize: 11, color: colors.textMuted, marginTop: 4 },
14569
+ errorBanner: { backgroundColor: "#7f1d1d", borderWidth: 1, borderColor: "#991b1b", borderRadius: 8, padding: 12, marginTop: 16 },
14570
+ errorText: { fontSize: 13, color: "#fca5a5", lineHeight: 18 }
14546
14571
  });
14547
14572
 
14548
14573
  // src/widget/screens/ReportSuccessScreen.tsx
@@ -15245,9 +15270,108 @@ var styles13 = import_react_native15.StyleSheet.create({
15245
15270
  fontSize: 14
15246
15271
  }
15247
15272
  });
15273
+
15274
+ // src/BugBearErrorBoundary.tsx
15275
+ var import_react17 = __toESM(require("react"));
15276
+ var import_react_native16 = require("react-native");
15277
+ var BugBearErrorBoundary = class extends import_react17.Component {
15278
+ constructor(props) {
15279
+ super(props);
15280
+ this.reset = () => {
15281
+ this.setState({
15282
+ hasError: false,
15283
+ error: null,
15284
+ errorInfo: null
15285
+ });
15286
+ };
15287
+ this.state = {
15288
+ hasError: false,
15289
+ error: null,
15290
+ errorInfo: null
15291
+ };
15292
+ }
15293
+ static getDerivedStateFromError(error) {
15294
+ return { hasError: true, error };
15295
+ }
15296
+ componentDidCatch(error, errorInfo) {
15297
+ this.setState({ errorInfo });
15298
+ captureError(error, {
15299
+ componentStack: errorInfo.componentStack ?? void 0
15300
+ });
15301
+ console.error("BugBear: Error caught by ErrorBoundary", {
15302
+ error: error.message,
15303
+ componentStack: errorInfo.componentStack?.slice(0, 500)
15304
+ });
15305
+ this.props.onError?.(error, errorInfo);
15306
+ this.props.errorReporter?.(error, {
15307
+ componentStack: errorInfo.componentStack ?? void 0,
15308
+ source: "BugBearErrorBoundary"
15309
+ });
15310
+ }
15311
+ render() {
15312
+ const { hasError, error } = this.state;
15313
+ const { children, fallback } = this.props;
15314
+ if (hasError && error) {
15315
+ if (typeof fallback === "function") {
15316
+ return fallback(error, this.reset);
15317
+ }
15318
+ if (fallback) {
15319
+ return fallback;
15320
+ }
15321
+ return /* @__PURE__ */ import_react17.default.createElement(import_react_native16.View, { style: styles14.container }, /* @__PURE__ */ import_react17.default.createElement(import_react_native16.Text, { style: styles14.title }, "Something went wrong"), /* @__PURE__ */ import_react17.default.createElement(import_react_native16.Text, { style: styles14.message }, error.message), /* @__PURE__ */ import_react17.default.createElement(import_react_native16.TouchableOpacity, { style: styles14.button, onPress: this.reset }, /* @__PURE__ */ import_react17.default.createElement(import_react_native16.Text, { style: styles14.buttonText }, "Try Again")), /* @__PURE__ */ import_react17.default.createElement(import_react_native16.Text, { style: styles14.caption }, "The error has been captured by BugBear"));
15322
+ }
15323
+ return children;
15324
+ }
15325
+ };
15326
+ function useErrorContext() {
15327
+ return {
15328
+ captureError,
15329
+ getEnhancedContext: () => contextCapture.getEnhancedContext()
15330
+ };
15331
+ }
15332
+ var styles14 = import_react_native16.StyleSheet.create({
15333
+ container: {
15334
+ padding: 20,
15335
+ margin: 20,
15336
+ backgroundColor: "#fef2f2",
15337
+ borderWidth: 1,
15338
+ borderColor: "#fecaca",
15339
+ borderRadius: 8
15340
+ },
15341
+ title: {
15342
+ fontSize: 16,
15343
+ fontWeight: "600",
15344
+ color: "#991b1b",
15345
+ marginBottom: 8
15346
+ },
15347
+ message: {
15348
+ fontSize: 14,
15349
+ color: "#7f1d1d",
15350
+ marginBottom: 12
15351
+ },
15352
+ button: {
15353
+ backgroundColor: "#dc2626",
15354
+ paddingHorizontal: 16,
15355
+ paddingVertical: 8,
15356
+ borderRadius: 6,
15357
+ alignSelf: "flex-start"
15358
+ },
15359
+ buttonText: {
15360
+ color: "white",
15361
+ fontSize: 14,
15362
+ fontWeight: "500"
15363
+ },
15364
+ caption: {
15365
+ fontSize: 12,
15366
+ color: "#9ca3af",
15367
+ marginTop: 12
15368
+ }
15369
+ });
15248
15370
  // Annotate the CommonJS export names for ESM import in node:
15249
15371
  0 && (module.exports = {
15250
15372
  BugBearButton,
15373
+ BugBearErrorBoundary,
15251
15374
  BugBearProvider,
15252
- useBugBear
15375
+ useBugBear,
15376
+ useErrorContext
15253
15377
  });
package/dist/index.mjs CHANGED
@@ -11503,7 +11503,7 @@ var ContextCaptureManager = class {
11503
11503
  });
11504
11504
  }
11505
11505
  captureFetch() {
11506
- if (typeof window === "undefined" || typeof fetch === "undefined") return;
11506
+ if (typeof window === "undefined" || typeof fetch === "undefined" || typeof document === "undefined") return;
11507
11507
  this.originalFetch = window.fetch;
11508
11508
  const self2 = this;
11509
11509
  window.fetch = async function(input, init) {
@@ -11594,6 +11594,13 @@ var ContextCaptureManager = class {
11594
11594
  }
11595
11595
  };
11596
11596
  var contextCapture = new ContextCaptureManager();
11597
+ function captureError(error, errorInfo) {
11598
+ return {
11599
+ errorMessage: error.message,
11600
+ errorStack: error.stack,
11601
+ componentStack: errorInfo?.componentStack
11602
+ };
11603
+ }
11597
11604
  var DEFAULT_SUPABASE_URL = "https://kyxgzjnqgvapvlnvqawz.supabase.co";
11598
11605
  var getEnvVar = (key) => {
11599
11606
  try {
@@ -11761,7 +11768,7 @@ var BugBearClient = class {
11761
11768
  sort_order
11762
11769
  )
11763
11770
  )
11764
- `).eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).in("status", ["pending", "in_progress"]).order("created_at", { ascending: true });
11771
+ `).eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).in("status", ["pending", "in_progress"]).order("created_at", { ascending: true }).limit(100);
11765
11772
  if (error) {
11766
11773
  console.error("BugBear: Failed to fetch assignments", error);
11767
11774
  return [];
@@ -11915,7 +11922,7 @@ var BugBearClient = class {
11915
11922
  if (options?.testResult) {
11916
11923
  updateData.test_result = options.testResult;
11917
11924
  }
11918
- const { error } = await this.supabase.from("test_assignments").update(updateData).eq("id", assignmentId);
11925
+ const { data: updatedRow, error } = await this.supabase.from("test_assignments").update(updateData).eq("id", assignmentId).eq("status", currentAssignment.status).select("id").maybeSingle();
11919
11926
  if (error) {
11920
11927
  console.error("BugBear: Failed to update assignment status", {
11921
11928
  message: error.message,
@@ -11928,6 +11935,9 @@ var BugBearClient = class {
11928
11935
  });
11929
11936
  return { success: false, error: error.message };
11930
11937
  }
11938
+ if (!updatedRow) {
11939
+ return { success: false, error: "Assignment status has changed. Please refresh and try again." };
11940
+ }
11931
11941
  if (options?.feedback && ["passed", "failed", "blocked"].includes(status)) {
11932
11942
  const { data: assignmentData, error: fetchError2 } = await this.supabase.from("test_assignments").select("test_case_id").eq("id", assignmentId).single();
11933
11943
  if (fetchError2) {
@@ -12154,6 +12164,28 @@ var BugBearClient = class {
12154
12164
  return null;
12155
12165
  }
12156
12166
  }
12167
+ /**
12168
+ * Get detailed assignment stats for the current tester via RPC.
12169
+ * Returns counts by status (pending, in_progress, passed, failed, blocked, skipped, total).
12170
+ */
12171
+ async getTesterStats() {
12172
+ try {
12173
+ const testerInfo = await this.getTesterInfo();
12174
+ if (!testerInfo) return null;
12175
+ const { data, error } = await this.supabase.rpc("get_tester_stats", {
12176
+ p_project_id: this.config.projectId,
12177
+ p_tester_id: testerInfo.id
12178
+ });
12179
+ if (error) {
12180
+ console.error("BugBear: Failed to fetch tester stats", error);
12181
+ return null;
12182
+ }
12183
+ return data;
12184
+ } catch (err) {
12185
+ console.error("BugBear: Error fetching tester stats", err);
12186
+ return null;
12187
+ }
12188
+ }
12157
12189
  /**
12158
12190
  * Basic email format validation (defense in depth)
12159
12191
  */
@@ -12487,75 +12519,35 @@ var BugBearClient = class {
12487
12519
  try {
12488
12520
  const testerInfo = await this.getTesterInfo();
12489
12521
  if (!testerInfo) return [];
12490
- const { data: threads, error } = await this.supabase.from("discussion_threads").select(`
12491
- id,
12492
- subject,
12493
- thread_type,
12494
- priority,
12495
- is_pinned,
12496
- is_resolved,
12497
- last_message_at,
12498
- created_at
12499
- `).eq("project_id", this.config.projectId).or(`audience.eq.all,audience_tester_ids.cs.{${testerInfo.id}}`).order("is_pinned", { ascending: false }).order("last_message_at", { ascending: false });
12522
+ const { data, error } = await this.supabase.rpc("get_threads_with_unread", {
12523
+ p_project_id: this.config.projectId,
12524
+ p_tester_id: testerInfo.id
12525
+ });
12500
12526
  if (error) {
12501
- console.error("BugBear: Failed to fetch threads", error);
12527
+ console.error("BugBear: Failed to fetch threads via RPC", error);
12502
12528
  return [];
12503
12529
  }
12504
- if (!threads || threads.length === 0) return [];
12505
- const threadIds = threads.map((t) => t.id);
12506
- const { data: readStatuses } = await this.supabase.from("discussion_read_status").select("thread_id, last_read_at, last_read_message_id").eq("tester_id", testerInfo.id).in("thread_id", threadIds);
12507
- const readStatusMap = new Map(
12508
- (readStatuses || []).map((rs) => [rs.thread_id, rs])
12509
- );
12510
- const { data: lastMessages } = await this.supabase.from("discussion_messages").select(`
12511
- id,
12512
- thread_id,
12513
- sender_type,
12514
- sender_name,
12515
- content,
12516
- created_at,
12517
- attachments
12518
- `).in("thread_id", threadIds).order("created_at", { ascending: false });
12519
- const lastMessageMap = /* @__PURE__ */ new Map();
12520
- for (const msg of lastMessages || []) {
12521
- if (!lastMessageMap.has(msg.thread_id)) {
12522
- lastMessageMap.set(msg.thread_id, msg);
12523
- }
12524
- }
12525
- const unreadCounts = await Promise.all(
12526
- threads.map(async (thread) => {
12527
- const readStatus = readStatusMap.get(thread.id);
12528
- const lastReadAt = readStatus?.last_read_at || "1970-01-01T00:00:00Z";
12529
- const { count, error: countError } = await this.supabase.from("discussion_messages").select("*", { count: "exact", head: true }).eq("thread_id", thread.id).gt("created_at", lastReadAt);
12530
- return { threadId: thread.id, count: countError ? 0 : count || 0 };
12531
- })
12532
- );
12533
- const unreadCountMap = new Map(
12534
- unreadCounts.map((uc) => [uc.threadId, uc.count])
12535
- );
12536
- return threads.map((thread) => {
12537
- const lastMsg = lastMessageMap.get(thread.id);
12538
- return {
12539
- id: thread.id,
12540
- subject: thread.subject,
12541
- threadType: thread.thread_type,
12542
- priority: thread.priority,
12543
- isPinned: thread.is_pinned,
12544
- isResolved: thread.is_resolved,
12545
- lastMessageAt: thread.last_message_at,
12546
- createdAt: thread.created_at,
12547
- unreadCount: unreadCountMap.get(thread.id) || 0,
12548
- lastMessage: lastMsg ? {
12549
- id: lastMsg.id,
12550
- threadId: lastMsg.thread_id,
12551
- senderType: lastMsg.sender_type,
12552
- senderName: lastMsg.sender_name,
12553
- content: lastMsg.content,
12554
- createdAt: lastMsg.created_at,
12555
- attachments: lastMsg.attachments || []
12556
- } : void 0
12557
- };
12558
- });
12530
+ if (!data || data.length === 0) return [];
12531
+ return data.map((row) => ({
12532
+ id: row.thread_id,
12533
+ subject: row.thread_subject,
12534
+ threadType: row.thread_type,
12535
+ priority: row.thread_priority,
12536
+ isPinned: row.is_pinned,
12537
+ isResolved: row.is_resolved,
12538
+ lastMessageAt: row.last_message_at,
12539
+ createdAt: row.created_at,
12540
+ unreadCount: Number(row.unread_count) || 0,
12541
+ lastMessage: row.last_message_preview ? {
12542
+ id: "",
12543
+ threadId: row.thread_id,
12544
+ senderType: row.last_message_sender_type || "system",
12545
+ senderName: row.last_message_sender_name || "",
12546
+ content: row.last_message_preview,
12547
+ createdAt: row.last_message_at,
12548
+ attachments: []
12549
+ } : void 0
12550
+ }));
12559
12551
  } catch (err) {
12560
12552
  console.error("BugBear: Error fetching threads", err);
12561
12553
  return [];
@@ -12574,7 +12566,7 @@ var BugBearClient = class {
12574
12566
  content,
12575
12567
  created_at,
12576
12568
  attachments
12577
- `).eq("thread_id", threadId).order("created_at", { ascending: true });
12569
+ `).eq("thread_id", threadId).order("created_at", { ascending: true }).limit(200);
12578
12570
  if (error) {
12579
12571
  console.error("BugBear: Failed to fetch messages", error);
12580
12572
  return [];
@@ -12623,7 +12615,6 @@ var BugBearClient = class {
12623
12615
  console.error("BugBear: Failed to send message", error);
12624
12616
  return false;
12625
12617
  }
12626
- await this.supabase.from("discussion_threads").update({ last_message_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", threadId);
12627
12618
  await this.markThreadAsRead(threadId);
12628
12619
  return true;
12629
12620
  } catch (err) {
@@ -12854,7 +12845,7 @@ var BugBearClient = class {
12854
12845
  */
12855
12846
  async getSessionFindings(sessionId) {
12856
12847
  try {
12857
- const { data, error } = await this.supabase.from("qa_findings").select("*").eq("session_id", sessionId).order("created_at", { ascending: true });
12848
+ const { data, error } = await this.supabase.from("qa_findings").select("*").eq("session_id", sessionId).order("created_at", { ascending: true }).limit(100);
12858
12849
  if (error) {
12859
12850
  console.error("BugBear: Failed to fetch findings", error);
12860
12851
  return [];
@@ -12998,7 +12989,8 @@ var BugBearContext = createContext({
12998
12989
  updateTesterProfile: async () => ({ success: false }),
12999
12990
  refreshTesterInfo: async () => {
13000
12991
  },
13001
- dashboardUrl: void 0
12992
+ dashboardUrl: void 0,
12993
+ onError: void 0
13002
12994
  });
13003
12995
  function useBugBear() {
13004
12996
  return useContext(BugBearContext);
@@ -13147,6 +13139,9 @@ function BugBearProvider({ config, children, appVersion, enabled = true }) {
13147
13139
  }
13148
13140
  } catch (err) {
13149
13141
  console.error("BugBear: Init error", err);
13142
+ if (err instanceof Error) {
13143
+ config.onError?.(err, { phase: "init" });
13144
+ }
13150
13145
  } finally {
13151
13146
  setIsLoading(false);
13152
13147
  }
@@ -13204,7 +13199,8 @@ function BugBearProvider({ config, children, appVersion, enabled = true }) {
13204
13199
  refreshTesterStatus,
13205
13200
  updateTesterProfile,
13206
13201
  refreshTesterInfo,
13207
- dashboardUrl: config.dashboardUrl
13202
+ dashboardUrl: config.dashboardUrl,
13203
+ onError: config.onError
13208
13204
  }
13209
13205
  },
13210
13206
  children
@@ -13729,6 +13725,7 @@ function TestDetailScreen({ testId, nav }) {
13729
13725
  const [selectedSkipReason, setSelectedSkipReason] = useState2(null);
13730
13726
  const [skipNotes, setSkipNotes] = useState2("");
13731
13727
  const [skipping, setSkipping] = useState2(false);
13728
+ const [isSubmitting, setIsSubmitting] = useState2(false);
13732
13729
  useEffect3(() => {
13733
13730
  setCriteriaResults({});
13734
13731
  setShowSteps(true);
@@ -13751,26 +13748,36 @@ function TestDetailScreen({ testId, nav }) {
13751
13748
  const allTests = assignments;
13752
13749
  const currentIndex = displayedAssignment ? assignments.indexOf(displayedAssignment) : -1;
13753
13750
  const handlePass = useCallback2(async () => {
13754
- if (!client || !displayedAssignment) return;
13751
+ if (!client || !displayedAssignment || isSubmitting) return;
13755
13752
  Keyboard.dismiss();
13756
- await client.passAssignment(displayedAssignment.id);
13757
- await refreshAssignments();
13758
- nav.replace({ name: "TEST_FEEDBACK", status: "passed", assignmentId: displayedAssignment.id });
13759
- }, [client, displayedAssignment, refreshAssignments, nav]);
13753
+ setIsSubmitting(true);
13754
+ try {
13755
+ await client.passAssignment(displayedAssignment.id);
13756
+ await refreshAssignments();
13757
+ nav.replace({ name: "TEST_FEEDBACK", status: "passed", assignmentId: displayedAssignment.id });
13758
+ } finally {
13759
+ setIsSubmitting(false);
13760
+ }
13761
+ }, [client, displayedAssignment, refreshAssignments, nav, isSubmitting]);
13760
13762
  const handleFail = useCallback2(async () => {
13761
- if (!client || !displayedAssignment) return;
13763
+ if (!client || !displayedAssignment || isSubmitting) return;
13762
13764
  Keyboard.dismiss();
13763
- await client.failAssignment(displayedAssignment.id);
13764
- await refreshAssignments();
13765
- nav.replace({
13766
- name: "REPORT",
13767
- prefill: {
13768
- type: "test_fail",
13769
- assignmentId: displayedAssignment.id,
13770
- testCaseId: displayedAssignment.testCase.id
13771
- }
13772
- });
13773
- }, [client, displayedAssignment, refreshAssignments, nav]);
13765
+ setIsSubmitting(true);
13766
+ try {
13767
+ await client.failAssignment(displayedAssignment.id);
13768
+ await refreshAssignments();
13769
+ nav.replace({
13770
+ name: "REPORT",
13771
+ prefill: {
13772
+ type: "test_fail",
13773
+ assignmentId: displayedAssignment.id,
13774
+ testCaseId: displayedAssignment.testCase.id
13775
+ }
13776
+ });
13777
+ } finally {
13778
+ setIsSubmitting(false);
13779
+ }
13780
+ }, [client, displayedAssignment, refreshAssignments, nav, isSubmitting]);
13774
13781
  const handleSkip = useCallback2(async () => {
13775
13782
  if (!client || !displayedAssignment || !selectedSkipReason) return;
13776
13783
  Keyboard.dismiss();
@@ -13851,7 +13858,7 @@ function TestDetailScreen({ testId, nav }) {
13851
13858
  }
13852
13859
  },
13853
13860
  /* @__PURE__ */ React3.createElement(Text2, { style: styles2.navigateText }, "\u{1F9ED} Go to test location")
13854
- ), /* @__PURE__ */ React3.createElement(TouchableOpacity2, { onPress: () => setShowDetails(!showDetails), style: styles2.detailsToggle }, /* @__PURE__ */ React3.createElement(Text2, { style: styles2.detailsToggleText }, showDetails ? "\u25BC" : "\u25B6", " Details")), showDetails && /* @__PURE__ */ React3.createElement(View2, { style: styles2.detailsSection }, testCase.key && /* @__PURE__ */ React3.createElement(Text2, { style: styles2.detailMeta }, testCase.key, " \xB7 ", testCase.priority, " \xB7 ", info.name), testCase.description && /* @__PURE__ */ React3.createElement(Text2, { style: styles2.detailDesc }, testCase.description), testCase.group && /* @__PURE__ */ React3.createElement(View2, { style: styles2.folderProgress }, /* @__PURE__ */ React3.createElement(Text2, { style: styles2.folderName }, "\u{1F4C1} ", testCase.group.name))), /* @__PURE__ */ React3.createElement(View2, { style: styles2.actionButtons }, /* @__PURE__ */ React3.createElement(TouchableOpacity2, { style: [styles2.actionBtn, styles2.failBtn], onPress: handleFail }, /* @__PURE__ */ React3.createElement(Text2, { style: styles2.failBtnText }, "Fail")), /* @__PURE__ */ React3.createElement(TouchableOpacity2, { style: [styles2.actionBtn, styles2.skipBtn], onPress: () => setShowSkipModal(true) }, /* @__PURE__ */ React3.createElement(Text2, { style: styles2.skipBtnText }, "Skip")), /* @__PURE__ */ React3.createElement(TouchableOpacity2, { style: [styles2.actionBtn, styles2.passBtn], onPress: handlePass }, /* @__PURE__ */ React3.createElement(Text2, { style: styles2.passBtnText }, "Pass"))), /* @__PURE__ */ React3.createElement(Modal, { visible: showSkipModal, transparent: true, animationType: "fade" }, /* @__PURE__ */ React3.createElement(View2, { style: styles2.modalOverlay }, /* @__PURE__ */ React3.createElement(View2, { style: styles2.modalContent }, /* @__PURE__ */ React3.createElement(Text2, { style: styles2.modalTitle }, "Skip this test?"), /* @__PURE__ */ React3.createElement(Text2, { style: styles2.modalSubtitle }, "Select a reason:"), [
13861
+ ), /* @__PURE__ */ React3.createElement(TouchableOpacity2, { onPress: () => setShowDetails(!showDetails), style: styles2.detailsToggle }, /* @__PURE__ */ React3.createElement(Text2, { style: styles2.detailsToggleText }, showDetails ? "\u25BC" : "\u25B6", " Details")), showDetails && /* @__PURE__ */ React3.createElement(View2, { style: styles2.detailsSection }, testCase.key && /* @__PURE__ */ React3.createElement(Text2, { style: styles2.detailMeta }, testCase.key, " \xB7 ", testCase.priority, " \xB7 ", info.name), testCase.description && /* @__PURE__ */ React3.createElement(Text2, { style: styles2.detailDesc }, testCase.description), testCase.group && /* @__PURE__ */ React3.createElement(View2, { style: styles2.folderProgress }, /* @__PURE__ */ React3.createElement(Text2, { style: styles2.folderName }, "\u{1F4C1} ", testCase.group.name))), /* @__PURE__ */ React3.createElement(View2, { style: styles2.actionButtons }, /* @__PURE__ */ React3.createElement(TouchableOpacity2, { style: [styles2.actionBtn, styles2.failBtn, isSubmitting && { opacity: 0.5 }], onPress: handleFail, disabled: isSubmitting }, /* @__PURE__ */ React3.createElement(Text2, { style: styles2.failBtnText }, isSubmitting ? "Failing..." : "Fail")), /* @__PURE__ */ React3.createElement(TouchableOpacity2, { style: [styles2.actionBtn, styles2.skipBtn, isSubmitting && { opacity: 0.5 }], onPress: () => setShowSkipModal(true), disabled: isSubmitting }, /* @__PURE__ */ React3.createElement(Text2, { style: styles2.skipBtnText }, "Skip")), /* @__PURE__ */ React3.createElement(TouchableOpacity2, { style: [styles2.actionBtn, styles2.passBtn, isSubmitting && { opacity: 0.5 }], onPress: handlePass, disabled: isSubmitting }, /* @__PURE__ */ React3.createElement(Text2, { style: styles2.passBtnText }, isSubmitting ? "Passing..." : "Pass"))), /* @__PURE__ */ React3.createElement(Modal, { visible: showSkipModal, transparent: true, animationType: "fade" }, /* @__PURE__ */ React3.createElement(View2, { style: styles2.modalOverlay }, /* @__PURE__ */ React3.createElement(View2, { style: styles2.modalContent }, /* @__PURE__ */ React3.createElement(Text2, { style: styles2.modalTitle }, "Skip this test?"), /* @__PURE__ */ React3.createElement(Text2, { style: styles2.modalSubtitle }, "Select a reason:"), [
13855
13862
  { reason: "blocked", label: "\u{1F6AB} Blocked by a bug" },
13856
13863
  { reason: "not_ready", label: "\u{1F6A7} Feature not ready" },
13857
13864
  { reason: "dependency", label: "\u{1F517} Needs another test first" },
@@ -14419,32 +14426,46 @@ function ReportScreen({ nav, prefill }) {
14419
14426
  const [description, setDescription] = useState6("");
14420
14427
  const [affectedScreen, setAffectedScreen] = useState6("");
14421
14428
  const [submitting, setSubmitting] = useState6(false);
14429
+ const [error, setError] = useState6(null);
14422
14430
  const images = useImageAttachments(uploadImage, 5, "screenshots");
14423
14431
  const isBugType = reportType === "bug" || reportType === "test_fail";
14424
14432
  const handleSubmit = async () => {
14425
14433
  if (!client || !description.trim()) return;
14426
14434
  setSubmitting(true);
14427
- const baseContext = client.getAppContext();
14428
- const appContext = {
14429
- ...baseContext,
14430
- currentRoute: affectedScreen.trim() || baseContext.currentRoute
14431
- };
14432
- const screenshotUrls = images.getScreenshotUrls();
14433
- await client.submitReport({
14434
- type: reportType,
14435
- description: description.trim(),
14436
- severity: isBugType ? severity : void 0,
14437
- assignmentId: prefill?.assignmentId,
14438
- testCaseId: prefill?.testCaseId,
14439
- appContext,
14440
- deviceInfo: getDeviceInfo(),
14441
- screenshots: screenshotUrls.length > 0 ? screenshotUrls : void 0
14442
- });
14443
- if (prefill?.assignmentId) {
14444
- await refreshAssignments();
14435
+ setError(null);
14436
+ try {
14437
+ const baseContext = client.getAppContext();
14438
+ const appContext = {
14439
+ ...baseContext,
14440
+ currentRoute: affectedScreen.trim() || baseContext.currentRoute
14441
+ };
14442
+ const screenshotUrls = images.getScreenshotUrls();
14443
+ const result = await client.submitReport({
14444
+ type: reportType,
14445
+ description: description.trim(),
14446
+ severity: isBugType ? severity : void 0,
14447
+ assignmentId: prefill?.assignmentId,
14448
+ testCaseId: prefill?.testCaseId,
14449
+ appContext,
14450
+ deviceInfo: getDeviceInfo(),
14451
+ screenshots: screenshotUrls.length > 0 ? screenshotUrls : void 0
14452
+ });
14453
+ if (!result.success) {
14454
+ console.error("BugBear: Report submission failed", result.error);
14455
+ setError(result.error || "Failed to submit report. Please try again.");
14456
+ setSubmitting(false);
14457
+ return;
14458
+ }
14459
+ if (prefill?.assignmentId) {
14460
+ await refreshAssignments();
14461
+ }
14462
+ setSubmitting(false);
14463
+ nav.replace({ name: "REPORT_SUCCESS" });
14464
+ } catch (err) {
14465
+ console.error("BugBear: Report submission error", err);
14466
+ setError(err instanceof Error ? err.message : "An unexpected error occurred. Please try again.");
14467
+ setSubmitting(false);
14445
14468
  }
14446
- setSubmitting(false);
14447
- nav.replace({ name: "REPORT_SUCCESS" });
14448
14469
  };
14449
14470
  return /* @__PURE__ */ React8.createElement(View7, null, /* @__PURE__ */ React8.createElement(Text7, { style: shared.label }, "What are you reporting?"), /* @__PURE__ */ React8.createElement(View7, { style: styles7.typeRow }, [
14450
14471
  { type: "bug", label: "Bug", icon: "\u{1F41B}" },
@@ -14503,14 +14524,14 @@ function ReportScreen({ nav, prefill }) {
14503
14524
  onRemove: images.removeImage,
14504
14525
  label: "Screenshots (optional)"
14505
14526
  }
14506
- ), /* @__PURE__ */ React8.createElement(
14527
+ ), error && /* @__PURE__ */ React8.createElement(View7, { style: styles7.errorBanner }, /* @__PURE__ */ React8.createElement(Text7, { style: styles7.errorText }, error)), /* @__PURE__ */ React8.createElement(
14507
14528
  TouchableOpacity7,
14508
14529
  {
14509
14530
  style: [shared.primaryButton, (!description.trim() || submitting || images.isUploading) && shared.primaryButtonDisabled, { marginTop: 20 }],
14510
14531
  onPress: handleSubmit,
14511
14532
  disabled: !description.trim() || submitting || images.isUploading
14512
14533
  },
14513
- /* @__PURE__ */ React8.createElement(Text7, { style: shared.primaryButtonText }, images.isUploading ? "Uploading images..." : submitting ? "Submitting..." : "Submit Report")
14534
+ /* @__PURE__ */ React8.createElement(Text7, { style: shared.primaryButtonText }, images.isUploading ? "Uploading images..." : submitting ? "Submitting..." : error ? "Retry" : "Submit Report")
14514
14535
  ));
14515
14536
  }
14516
14537
  var styles7 = StyleSheet8.create({
@@ -14526,7 +14547,9 @@ var styles7 = StyleSheet8.create({
14526
14547
  sevText: { fontSize: 12, fontWeight: "500", color: colors.textSecondary, textTransform: "capitalize" },
14527
14548
  descInput: { backgroundColor: colors.card, borderWidth: 1, borderColor: colors.border, borderRadius: 12, paddingHorizontal: 14, paddingVertical: 12, fontSize: 14, color: colors.textPrimary, minHeight: 100 },
14528
14549
  screenInput: { backgroundColor: colors.card, borderWidth: 1, borderColor: colors.border, borderRadius: 8, paddingHorizontal: 12, paddingVertical: 8, fontSize: 13, color: colors.textPrimary },
14529
- screenHint: { fontSize: 11, color: colors.textMuted, marginTop: 4 }
14550
+ screenHint: { fontSize: 11, color: colors.textMuted, marginTop: 4 },
14551
+ errorBanner: { backgroundColor: "#7f1d1d", borderWidth: 1, borderColor: "#991b1b", borderRadius: 8, padding: 12, marginTop: 16 },
14552
+ errorText: { fontSize: 13, color: "#fca5a5", lineHeight: 18 }
14530
14553
  });
14531
14554
 
14532
14555
  // src/widget/screens/ReportSuccessScreen.tsx
@@ -15229,8 +15252,107 @@ var styles13 = StyleSheet14.create({
15229
15252
  fontSize: 14
15230
15253
  }
15231
15254
  });
15255
+
15256
+ // src/BugBearErrorBoundary.tsx
15257
+ import React15, { Component } from "react";
15258
+ import { View as View14, Text as Text14, TouchableOpacity as TouchableOpacity13, StyleSheet as StyleSheet15 } from "react-native";
15259
+ var BugBearErrorBoundary = class extends Component {
15260
+ constructor(props) {
15261
+ super(props);
15262
+ this.reset = () => {
15263
+ this.setState({
15264
+ hasError: false,
15265
+ error: null,
15266
+ errorInfo: null
15267
+ });
15268
+ };
15269
+ this.state = {
15270
+ hasError: false,
15271
+ error: null,
15272
+ errorInfo: null
15273
+ };
15274
+ }
15275
+ static getDerivedStateFromError(error) {
15276
+ return { hasError: true, error };
15277
+ }
15278
+ componentDidCatch(error, errorInfo) {
15279
+ this.setState({ errorInfo });
15280
+ captureError(error, {
15281
+ componentStack: errorInfo.componentStack ?? void 0
15282
+ });
15283
+ console.error("BugBear: Error caught by ErrorBoundary", {
15284
+ error: error.message,
15285
+ componentStack: errorInfo.componentStack?.slice(0, 500)
15286
+ });
15287
+ this.props.onError?.(error, errorInfo);
15288
+ this.props.errorReporter?.(error, {
15289
+ componentStack: errorInfo.componentStack ?? void 0,
15290
+ source: "BugBearErrorBoundary"
15291
+ });
15292
+ }
15293
+ render() {
15294
+ const { hasError, error } = this.state;
15295
+ const { children, fallback } = this.props;
15296
+ if (hasError && error) {
15297
+ if (typeof fallback === "function") {
15298
+ return fallback(error, this.reset);
15299
+ }
15300
+ if (fallback) {
15301
+ return fallback;
15302
+ }
15303
+ return /* @__PURE__ */ React15.createElement(View14, { style: styles14.container }, /* @__PURE__ */ React15.createElement(Text14, { style: styles14.title }, "Something went wrong"), /* @__PURE__ */ React15.createElement(Text14, { style: styles14.message }, error.message), /* @__PURE__ */ React15.createElement(TouchableOpacity13, { style: styles14.button, onPress: this.reset }, /* @__PURE__ */ React15.createElement(Text14, { style: styles14.buttonText }, "Try Again")), /* @__PURE__ */ React15.createElement(Text14, { style: styles14.caption }, "The error has been captured by BugBear"));
15304
+ }
15305
+ return children;
15306
+ }
15307
+ };
15308
+ function useErrorContext() {
15309
+ return {
15310
+ captureError,
15311
+ getEnhancedContext: () => contextCapture.getEnhancedContext()
15312
+ };
15313
+ }
15314
+ var styles14 = StyleSheet15.create({
15315
+ container: {
15316
+ padding: 20,
15317
+ margin: 20,
15318
+ backgroundColor: "#fef2f2",
15319
+ borderWidth: 1,
15320
+ borderColor: "#fecaca",
15321
+ borderRadius: 8
15322
+ },
15323
+ title: {
15324
+ fontSize: 16,
15325
+ fontWeight: "600",
15326
+ color: "#991b1b",
15327
+ marginBottom: 8
15328
+ },
15329
+ message: {
15330
+ fontSize: 14,
15331
+ color: "#7f1d1d",
15332
+ marginBottom: 12
15333
+ },
15334
+ button: {
15335
+ backgroundColor: "#dc2626",
15336
+ paddingHorizontal: 16,
15337
+ paddingVertical: 8,
15338
+ borderRadius: 6,
15339
+ alignSelf: "flex-start"
15340
+ },
15341
+ buttonText: {
15342
+ color: "white",
15343
+ fontSize: 14,
15344
+ fontWeight: "500"
15345
+ },
15346
+ caption: {
15347
+ fontSize: 12,
15348
+ color: "#9ca3af",
15349
+ marginTop: 12
15350
+ }
15351
+ });
15232
15352
  export {
15233
15353
  BugBearButton,
15354
+ BugBearErrorBoundary,
15234
15355
  BugBearProvider,
15235
- useBugBear
15356
+ useBugBear,
15357
+ useErrorContext
15236
15358
  };
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@bbearai/react-native",
3
- "version": "0.3.10",
3
+ "version": "0.4.0",
4
4
  "description": "BugBear React Native components for mobile apps",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
7
7
  "types": "./dist/index.d.ts",
8
8
  "exports": {
9
9
  ".": {
10
+ "types": "./dist/index.d.ts",
10
11
  "import": "./dist/index.mjs",
11
- "require": "./dist/index.js",
12
- "types": "./dist/index.d.ts"
12
+ "require": "./dist/index.js"
13
13
  }
14
14
  },
15
15
  "files": [
@@ -49,7 +49,7 @@
49
49
  }
50
50
  },
51
51
  "devDependencies": {
52
- "@bbearai/core": "^0.2.0",
52
+ "@bbearai/core": "^0.3.0",
53
53
  "@eslint/js": "^9.39.2",
54
54
  "@types/react": "^18.2.0",
55
55
  "eslint": "^9.39.2",