@bbearai/react-native 0.3.11 → 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
 
@@ -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" },
@@ -15261,9 +15270,108 @@ var styles13 = import_react_native15.StyleSheet.create({
15261
15270
  fontSize: 14
15262
15271
  }
15263
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
+ });
15264
15370
  // Annotate the CommonJS export names for ESM import in node:
15265
15371
  0 && (module.exports = {
15266
15372
  BugBearButton,
15373
+ BugBearErrorBoundary,
15267
15374
  BugBearProvider,
15268
- useBugBear
15375
+ useBugBear,
15376
+ useErrorContext
15269
15377
  });
package/dist/index.mjs CHANGED
@@ -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" },
@@ -15245,8 +15252,107 @@ var styles13 = StyleSheet14.create({
15245
15252
  fontSize: 14
15246
15253
  }
15247
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
+ });
15248
15352
  export {
15249
15353
  BugBearButton,
15354
+ BugBearErrorBoundary,
15250
15355
  BugBearProvider,
15251
- useBugBear
15356
+ useBugBear,
15357
+ useErrorContext
15252
15358
  };
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@bbearai/react-native",
3
- "version": "0.3.11",
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",