@bbearai/react-native 0.3.11 → 0.4.1

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.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,18 @@ 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
+ }
11637
+ var formatPgError = (e) => {
11638
+ if (!e || typeof e !== "object") return { raw: e };
11639
+ const { message, code, details, hint } = e;
11640
+ return { message, code, details, hint };
11641
+ };
11628
11642
  var DEFAULT_SUPABASE_URL = "https://kyxgzjnqgvapvlnvqawz.supabase.co";
11629
11643
  var getEnvVar = (key) => {
11630
11644
  try {
@@ -11792,9 +11806,9 @@ var BugBearClient = class {
11792
11806
  sort_order
11793
11807
  )
11794
11808
  )
11795
- `).eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).in("status", ["pending", "in_progress"]).order("created_at", { ascending: true });
11809
+ `).eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).in("status", ["pending", "in_progress"]).order("created_at", { ascending: true }).limit(100);
11796
11810
  if (error) {
11797
- console.error("BugBear: Failed to fetch assignments", error);
11811
+ console.error("BugBear: Failed to fetch assignments", formatPgError(error));
11798
11812
  return [];
11799
11813
  }
11800
11814
  const mapped = (data || []).map((item) => ({
@@ -11946,7 +11960,7 @@ var BugBearClient = class {
11946
11960
  if (options?.testResult) {
11947
11961
  updateData.test_result = options.testResult;
11948
11962
  }
11949
- const { error } = await this.supabase.from("test_assignments").update(updateData).eq("id", assignmentId);
11963
+ const { data: updatedRow, error } = await this.supabase.from("test_assignments").update(updateData).eq("id", assignmentId).eq("status", currentAssignment.status).select("id").maybeSingle();
11950
11964
  if (error) {
11951
11965
  console.error("BugBear: Failed to update assignment status", {
11952
11966
  message: error.message,
@@ -11959,6 +11973,9 @@ var BugBearClient = class {
11959
11973
  });
11960
11974
  return { success: false, error: error.message };
11961
11975
  }
11976
+ if (!updatedRow) {
11977
+ return { success: false, error: "Assignment status has changed. Please refresh and try again." };
11978
+ }
11962
11979
  if (options?.feedback && ["passed", "failed", "blocked"].includes(status)) {
11963
11980
  const { data: assignmentData, error: fetchError2 } = await this.supabase.from("test_assignments").select("test_case_id").eq("id", assignmentId).single();
11964
11981
  if (fetchError2) {
@@ -12019,7 +12036,7 @@ var BugBearClient = class {
12019
12036
  }
12020
12037
  const { error } = await this.supabase.from("test_assignments").update(updateData).eq("id", assignmentId);
12021
12038
  if (error) {
12022
- console.error("BugBear: Failed to skip assignment", error);
12039
+ console.error("BugBear: Failed to skip assignment", formatPgError(error));
12023
12040
  return { success: false, error: error.message };
12024
12041
  }
12025
12042
  return { success: true };
@@ -12185,6 +12202,28 @@ var BugBearClient = class {
12185
12202
  return null;
12186
12203
  }
12187
12204
  }
12205
+ /**
12206
+ * Get detailed assignment stats for the current tester via RPC.
12207
+ * Returns counts by status (pending, in_progress, passed, failed, blocked, skipped, total).
12208
+ */
12209
+ async getTesterStats() {
12210
+ try {
12211
+ const testerInfo = await this.getTesterInfo();
12212
+ if (!testerInfo) return null;
12213
+ const { data, error } = await this.supabase.rpc("get_tester_stats", {
12214
+ p_project_id: this.config.projectId,
12215
+ p_tester_id: testerInfo.id
12216
+ });
12217
+ if (error) {
12218
+ console.error("BugBear: Failed to fetch tester stats", formatPgError(error));
12219
+ return null;
12220
+ }
12221
+ return data;
12222
+ } catch (err) {
12223
+ console.error("BugBear: Error fetching tester stats", err);
12224
+ return null;
12225
+ }
12226
+ }
12188
12227
  /**
12189
12228
  * Basic email format validation (defense in depth)
12190
12229
  */
@@ -12327,7 +12366,7 @@ var BugBearClient = class {
12327
12366
  if (updates.platforms !== void 0) updateData.platforms = updates.platforms;
12328
12367
  const { error } = await this.supabase.from("testers").update(updateData).eq("id", testerInfo.id);
12329
12368
  if (error) {
12330
- console.error("BugBear: updateTesterProfile error", error);
12369
+ console.error("BugBear: updateTesterProfile error", formatPgError(error));
12331
12370
  return { success: false, error: error.message };
12332
12371
  }
12333
12372
  return { success: true };
@@ -12349,14 +12388,14 @@ var BugBearClient = class {
12349
12388
  */
12350
12389
  async isQAEnabled() {
12351
12390
  try {
12352
- const { data, error } = await this.supabase.from("projects").select("is_qa_enabled").eq("id", this.config.projectId).single();
12391
+ const { data, error } = await this.supabase.rpc("check_qa_enabled", {
12392
+ p_project_id: this.config.projectId
12393
+ });
12353
12394
  if (error) {
12354
- if (error.code !== "PGRST116") {
12355
- console.warn("BugBear: Could not check QA status", error.message || error.code || "Unknown error");
12356
- }
12395
+ console.warn("BugBear: Could not check QA status", error.message || error.code || "Unknown error");
12357
12396
  return true;
12358
12397
  }
12359
- return data?.is_qa_enabled ?? true;
12398
+ return data ?? true;
12360
12399
  } catch (err) {
12361
12400
  return true;
12362
12401
  }
@@ -12386,7 +12425,7 @@ var BugBearClient = class {
12386
12425
  upsert: false
12387
12426
  });
12388
12427
  if (error) {
12389
- console.error("BugBear: Failed to upload screenshot", error);
12428
+ console.error("BugBear: Failed to upload screenshot", formatPgError(error));
12390
12429
  return null;
12391
12430
  }
12392
12431
  const { data: { publicUrl } } = this.supabase.storage.from(bucket).getPublicUrl(path);
@@ -12417,7 +12456,7 @@ var BugBearClient = class {
12417
12456
  upsert: false
12418
12457
  });
12419
12458
  if (error) {
12420
- console.error("BugBear: Failed to upload image from URI", error);
12459
+ console.error("BugBear: Failed to upload image from URI", formatPgError(error));
12421
12460
  return null;
12422
12461
  }
12423
12462
  const { data: { publicUrl } } = this.supabase.storage.from(bucket).getPublicUrl(path);
@@ -12492,7 +12531,7 @@ var BugBearClient = class {
12492
12531
  }
12493
12532
  const { data, error } = await query;
12494
12533
  if (error) {
12495
- console.error("BugBear: Failed to fetch fix requests", error);
12534
+ console.error("BugBear: Failed to fetch fix requests", formatPgError(error));
12496
12535
  return [];
12497
12536
  }
12498
12537
  return (data || []).map((fr) => ({
@@ -12518,75 +12557,35 @@ var BugBearClient = class {
12518
12557
  try {
12519
12558
  const testerInfo = await this.getTesterInfo();
12520
12559
  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 });
12560
+ const { data, error } = await this.supabase.rpc("get_threads_with_unread", {
12561
+ p_project_id: this.config.projectId,
12562
+ p_tester_id: testerInfo.id
12563
+ });
12531
12564
  if (error) {
12532
- console.error("BugBear: Failed to fetch threads", error);
12565
+ console.error("BugBear: Failed to fetch threads via RPC", formatPgError(error));
12533
12566
  return [];
12534
12567
  }
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
- });
12568
+ if (!data || data.length === 0) return [];
12569
+ return data.map((row) => ({
12570
+ id: row.thread_id,
12571
+ subject: row.thread_subject,
12572
+ threadType: row.thread_type,
12573
+ priority: row.thread_priority,
12574
+ isPinned: row.is_pinned,
12575
+ isResolved: row.is_resolved,
12576
+ lastMessageAt: row.last_message_at,
12577
+ createdAt: row.created_at,
12578
+ unreadCount: Number(row.unread_count) || 0,
12579
+ lastMessage: row.last_message_preview ? {
12580
+ id: "",
12581
+ threadId: row.thread_id,
12582
+ senderType: row.last_message_sender_type || "system",
12583
+ senderName: row.last_message_sender_name || "",
12584
+ content: row.last_message_preview,
12585
+ createdAt: row.last_message_at,
12586
+ attachments: []
12587
+ } : void 0
12588
+ }));
12590
12589
  } catch (err) {
12591
12590
  console.error("BugBear: Error fetching threads", err);
12592
12591
  return [];
@@ -12605,9 +12604,9 @@ var BugBearClient = class {
12605
12604
  content,
12606
12605
  created_at,
12607
12606
  attachments
12608
- `).eq("thread_id", threadId).order("created_at", { ascending: true });
12607
+ `).eq("thread_id", threadId).order("created_at", { ascending: true }).limit(200);
12609
12608
  if (error) {
12610
- console.error("BugBear: Failed to fetch messages", error);
12609
+ console.error("BugBear: Failed to fetch messages", formatPgError(error));
12611
12610
  return [];
12612
12611
  }
12613
12612
  return (data || []).map((msg) => ({
@@ -12651,10 +12650,9 @@ var BugBearClient = class {
12651
12650
  }
12652
12651
  const { error } = await this.supabase.from("discussion_messages").insert(insertData);
12653
12652
  if (error) {
12654
- console.error("BugBear: Failed to send message", error);
12653
+ console.error("BugBear: Failed to send message", formatPgError(error));
12655
12654
  return false;
12656
12655
  }
12657
- await this.supabase.from("discussion_threads").update({ last_message_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", threadId);
12658
12656
  await this.markThreadAsRead(threadId);
12659
12657
  return true;
12660
12658
  } catch (err) {
@@ -12768,7 +12766,7 @@ var BugBearClient = class {
12768
12766
  p_platform: options.platform || null
12769
12767
  });
12770
12768
  if (error) {
12771
- console.error("BugBear: Failed to start session", error);
12769
+ console.error("BugBear: Failed to start session", formatPgError(error));
12772
12770
  return { success: false, error: error.message };
12773
12771
  }
12774
12772
  const session = await this.getSession(data);
@@ -12793,7 +12791,7 @@ var BugBearClient = class {
12793
12791
  p_routes_covered: options.routesCovered || null
12794
12792
  });
12795
12793
  if (error) {
12796
- console.error("BugBear: Failed to end session", error);
12794
+ console.error("BugBear: Failed to end session", formatPgError(error));
12797
12795
  return { success: false, error: error.message };
12798
12796
  }
12799
12797
  const session = this.transformSession(data);
@@ -12841,7 +12839,7 @@ var BugBearClient = class {
12841
12839
  if (!testerInfo) return [];
12842
12840
  const { data, error } = await this.supabase.from("qa_sessions").select("*").eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).order("started_at", { ascending: false }).limit(limit);
12843
12841
  if (error) {
12844
- console.error("BugBear: Failed to fetch session history", error);
12842
+ console.error("BugBear: Failed to fetch session history", formatPgError(error));
12845
12843
  return [];
12846
12844
  }
12847
12845
  return (data || []).map((s) => this.transformSession(s));
@@ -12869,7 +12867,7 @@ var BugBearClient = class {
12869
12867
  p_app_context: options.appContext || null
12870
12868
  });
12871
12869
  if (error) {
12872
- console.error("BugBear: Failed to add finding", error);
12870
+ console.error("BugBear: Failed to add finding", formatPgError(error));
12873
12871
  return { success: false, error: error.message };
12874
12872
  }
12875
12873
  const finding = this.transformFinding(data);
@@ -12885,9 +12883,9 @@ var BugBearClient = class {
12885
12883
  */
12886
12884
  async getSessionFindings(sessionId) {
12887
12885
  try {
12888
- const { data, error } = await this.supabase.from("qa_findings").select("*").eq("session_id", sessionId).order("created_at", { ascending: true });
12886
+ const { data, error } = await this.supabase.from("qa_findings").select("*").eq("session_id", sessionId).order("created_at", { ascending: true }).limit(100);
12889
12887
  if (error) {
12890
- console.error("BugBear: Failed to fetch findings", error);
12888
+ console.error("BugBear: Failed to fetch findings", formatPgError(error));
12891
12889
  return [];
12892
12890
  }
12893
12891
  return (data || []).map((f) => this.transformFinding(f));
@@ -12905,7 +12903,7 @@ var BugBearClient = class {
12905
12903
  p_finding_id: findingId
12906
12904
  });
12907
12905
  if (error) {
12908
- console.error("BugBear: Failed to convert finding", error);
12906
+ console.error("BugBear: Failed to convert finding", formatPgError(error));
12909
12907
  return { success: false, error: error.message };
12910
12908
  }
12911
12909
  return { success: true, bugId: data };
@@ -12926,7 +12924,7 @@ var BugBearClient = class {
12926
12924
  dismissed_at: (/* @__PURE__ */ new Date()).toISOString()
12927
12925
  }).eq("id", findingId);
12928
12926
  if (error) {
12929
- console.error("BugBear: Failed to dismiss finding", error);
12927
+ console.error("BugBear: Failed to dismiss finding", formatPgError(error));
12930
12928
  return { success: false, error: error.message };
12931
12929
  }
12932
12930
  return { success: true };
@@ -13029,7 +13027,8 @@ var BugBearContext = (0, import_react.createContext)({
13029
13027
  updateTesterProfile: async () => ({ success: false }),
13030
13028
  refreshTesterInfo: async () => {
13031
13029
  },
13032
- dashboardUrl: void 0
13030
+ dashboardUrl: void 0,
13031
+ onError: void 0
13033
13032
  });
13034
13033
  function useBugBear() {
13035
13034
  return (0, import_react.useContext)(BugBearContext);
@@ -13178,6 +13177,9 @@ function BugBearProvider({ config, children, appVersion, enabled = true }) {
13178
13177
  }
13179
13178
  } catch (err) {
13180
13179
  console.error("BugBear: Init error", err);
13180
+ if (err instanceof Error) {
13181
+ config.onError?.(err, { phase: "init" });
13182
+ }
13181
13183
  } finally {
13182
13184
  setIsLoading(false);
13183
13185
  }
@@ -13235,7 +13237,8 @@ function BugBearProvider({ config, children, appVersion, enabled = true }) {
13235
13237
  refreshTesterStatus,
13236
13238
  updateTesterProfile,
13237
13239
  refreshTesterInfo,
13238
- dashboardUrl: config.dashboardUrl
13240
+ dashboardUrl: config.dashboardUrl,
13241
+ onError: config.onError
13239
13242
  }
13240
13243
  },
13241
13244
  children
@@ -13745,6 +13748,7 @@ function TestDetailScreen({ testId, nav }) {
13745
13748
  const [selectedSkipReason, setSelectedSkipReason] = (0, import_react4.useState)(null);
13746
13749
  const [skipNotes, setSkipNotes] = (0, import_react4.useState)("");
13747
13750
  const [skipping, setSkipping] = (0, import_react4.useState)(false);
13751
+ const [isSubmitting, setIsSubmitting] = (0, import_react4.useState)(false);
13748
13752
  (0, import_react4.useEffect)(() => {
13749
13753
  setCriteriaResults({});
13750
13754
  setShowSteps(true);
@@ -13767,26 +13771,39 @@ function TestDetailScreen({ testId, nav }) {
13767
13771
  const allTests = assignments;
13768
13772
  const currentIndex = displayedAssignment ? assignments.indexOf(displayedAssignment) : -1;
13769
13773
  const handlePass = (0, import_react4.useCallback)(async () => {
13770
- if (!client || !displayedAssignment) return;
13774
+ if (!client || !displayedAssignment || isSubmitting) return;
13771
13775
  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]);
13776
+ setIsSubmitting(true);
13777
+ try {
13778
+ await client.passAssignment(displayedAssignment.id);
13779
+ await refreshAssignments();
13780
+ nav.replace({ name: "TEST_FEEDBACK", status: "passed", assignmentId: displayedAssignment.id });
13781
+ } finally {
13782
+ setIsSubmitting(false);
13783
+ }
13784
+ }, [client, displayedAssignment, refreshAssignments, nav, isSubmitting]);
13776
13785
  const handleFail = (0, import_react4.useCallback)(async () => {
13777
- if (!client || !displayedAssignment) return;
13786
+ if (!client || !displayedAssignment || isSubmitting) return;
13778
13787
  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
13788
+ setIsSubmitting(true);
13789
+ try {
13790
+ const result = await client.failAssignment(displayedAssignment.id);
13791
+ if (!result.success) {
13792
+ console.error("BugBear: Failed to mark assignment as failed", result.error);
13787
13793
  }
13788
- });
13789
- }, [client, displayedAssignment, refreshAssignments, nav]);
13794
+ await refreshAssignments();
13795
+ nav.replace({
13796
+ name: "REPORT",
13797
+ prefill: {
13798
+ type: "test_fail",
13799
+ assignmentId: displayedAssignment.id,
13800
+ testCaseId: displayedAssignment.testCase.id
13801
+ }
13802
+ });
13803
+ } finally {
13804
+ setIsSubmitting(false);
13805
+ }
13806
+ }, [client, displayedAssignment, refreshAssignments, nav, isSubmitting]);
13790
13807
  const handleSkip = (0, import_react4.useCallback)(async () => {
13791
13808
  if (!client || !displayedAssignment || !selectedSkipReason) return;
13792
13809
  import_react_native4.Keyboard.dismiss();
@@ -13867,7 +13884,7 @@ function TestDetailScreen({ testId, nav }) {
13867
13884
  }
13868
13885
  },
13869
13886
  /* @__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:"), [
13887
+ ), /* @__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
13888
  { reason: "blocked", label: "\u{1F6AB} Blocked by a bug" },
13872
13889
  { reason: "not_ready", label: "\u{1F6A7} Feature not ready" },
13873
13890
  { reason: "dependency", label: "\u{1F517} Needs another test first" },
@@ -14437,6 +14454,7 @@ function ReportScreen({ nav, prefill }) {
14437
14454
  const [submitting, setSubmitting] = (0, import_react10.useState)(false);
14438
14455
  const [error, setError] = (0, import_react10.useState)(null);
14439
14456
  const images = useImageAttachments(uploadImage, 5, "screenshots");
14457
+ const isRetestFailure = prefill?.type === "test_fail";
14440
14458
  const isBugType = reportType === "bug" || reportType === "test_fail";
14441
14459
  const handleSubmit = async () => {
14442
14460
  if (!client || !description.trim()) return;
@@ -14476,7 +14494,50 @@ function ReportScreen({ nav, prefill }) {
14476
14494
  setSubmitting(false);
14477
14495
  }
14478
14496
  };
14479
- 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 }, [
14497
+ return /* @__PURE__ */ import_react10.default.createElement(import_react_native9.View, null, isRetestFailure ? /* @__PURE__ */ import_react10.default.createElement(import_react10.default.Fragment, null, /* @__PURE__ */ import_react10.default.createElement(import_react_native9.View, { style: styles7.retestBanner }, /* @__PURE__ */ import_react10.default.createElement(import_react_native9.Text, { style: styles7.retestIcon }, "\u{1F504}"), /* @__PURE__ */ import_react10.default.createElement(import_react_native9.View, null, /* @__PURE__ */ import_react10.default.createElement(import_react_native9.Text, { style: styles7.retestTitle }, "Bug Still Present"), /* @__PURE__ */ import_react10.default.createElement(import_react_native9.Text, { style: styles7.retestSubtitle }, "The fix did not resolve this issue"))), /* @__PURE__ */ import_react10.default.createElement(import_react_native9.View, { style: styles7.section }, /* @__PURE__ */ import_react10.default.createElement(import_react_native9.Text, { style: shared.label }, "Severity"), /* @__PURE__ */ import_react10.default.createElement(import_react_native9.View, { style: styles7.severityRow }, [
14498
+ { sev: "critical", color: "#ef4444" },
14499
+ { sev: "high", color: "#f97316" },
14500
+ { sev: "medium", color: "#eab308" },
14501
+ { sev: "low", color: "#6b7280" }
14502
+ ].map(({ sev, color }) => /* @__PURE__ */ import_react10.default.createElement(
14503
+ import_react_native9.TouchableOpacity,
14504
+ {
14505
+ key: sev,
14506
+ style: [styles7.sevButton, severity === sev && { backgroundColor: `${color}30`, borderColor: color }],
14507
+ onPress: () => setSeverity(sev)
14508
+ },
14509
+ /* @__PURE__ */ import_react10.default.createElement(import_react_native9.Text, { style: [styles7.sevText, severity === sev && { color }] }, sev)
14510
+ )))), /* @__PURE__ */ import_react10.default.createElement(import_react_native9.View, { style: styles7.section }, /* @__PURE__ */ import_react10.default.createElement(import_react_native9.Text, { style: shared.label }, "What went wrong?"), /* @__PURE__ */ import_react10.default.createElement(
14511
+ import_react_native9.TextInput,
14512
+ {
14513
+ style: styles7.descInput,
14514
+ value: description,
14515
+ onChangeText: setDescription,
14516
+ placeholder: "Describe what you observed. What still doesn't work?",
14517
+ placeholderTextColor: colors.textMuted,
14518
+ multiline: true,
14519
+ numberOfLines: 4,
14520
+ textAlignVertical: "top"
14521
+ }
14522
+ )), /* @__PURE__ */ import_react10.default.createElement(
14523
+ ImagePickerButtons,
14524
+ {
14525
+ images: images.images,
14526
+ maxImages: 5,
14527
+ onPickGallery: images.pickFromGallery,
14528
+ onPickCamera: images.pickFromCamera,
14529
+ onRemove: images.removeImage,
14530
+ label: "Attachments (optional)"
14531
+ }
14532
+ ), 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(
14533
+ import_react_native9.TouchableOpacity,
14534
+ {
14535
+ style: [shared.primaryButton, styles7.retestSubmitButton, (!description.trim() || submitting || images.isUploading) && shared.primaryButtonDisabled, { marginTop: 20 }],
14536
+ onPress: handleSubmit,
14537
+ disabled: !description.trim() || submitting || images.isUploading
14538
+ },
14539
+ /* @__PURE__ */ import_react10.default.createElement(import_react_native9.Text, { style: shared.primaryButtonText }, images.isUploading ? "Uploading images..." : submitting ? "Submitting..." : error ? "Retry" : "Submit Failed Retest")
14540
+ )) : /* @__PURE__ */ import_react10.default.createElement(import_react10.default.Fragment, 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 }, [
14480
14541
  { type: "bug", label: "Bug", icon: "\u{1F41B}" },
14481
14542
  { type: "feedback", label: "Feedback", icon: "\u{1F4A1}" },
14482
14543
  { type: "suggestion", label: "Idea", icon: "\u2728" }
@@ -14541,7 +14602,7 @@ function ReportScreen({ nav, prefill }) {
14541
14602
  disabled: !description.trim() || submitting || images.isUploading
14542
14603
  },
14543
14604
  /* @__PURE__ */ import_react10.default.createElement(import_react_native9.Text, { style: shared.primaryButtonText }, images.isUploading ? "Uploading images..." : submitting ? "Submitting..." : error ? "Retry" : "Submit Report")
14544
- ));
14605
+ )));
14545
14606
  }
14546
14607
  var styles7 = import_react_native9.StyleSheet.create({
14547
14608
  typeRow: { flexDirection: "row", gap: 10, marginBottom: 20 },
@@ -14558,7 +14619,12 @@ var styles7 = import_react_native9.StyleSheet.create({
14558
14619
  screenInput: { backgroundColor: colors.card, borderWidth: 1, borderColor: colors.border, borderRadius: 8, paddingHorizontal: 12, paddingVertical: 8, fontSize: 13, color: colors.textPrimary },
14559
14620
  screenHint: { fontSize: 11, color: colors.textMuted, marginTop: 4 },
14560
14621
  errorBanner: { backgroundColor: "#7f1d1d", borderWidth: 1, borderColor: "#991b1b", borderRadius: 8, padding: 12, marginTop: 16 },
14561
- errorText: { fontSize: 13, color: "#fca5a5", lineHeight: 18 }
14622
+ errorText: { fontSize: 13, color: "#fca5a5", lineHeight: 18 },
14623
+ retestBanner: { flexDirection: "row", alignItems: "center", gap: 10, backgroundColor: "#422006", borderWidth: 1, borderColor: "#854d0e", borderRadius: 10, paddingVertical: 12, paddingHorizontal: 14, marginBottom: 20 },
14624
+ retestIcon: { fontSize: 16 },
14625
+ retestTitle: { fontSize: 15, fontWeight: "600", color: "#fbbf24", lineHeight: 20 },
14626
+ retestSubtitle: { fontSize: 12, color: "#d97706", lineHeight: 16 },
14627
+ retestSubmitButton: { backgroundColor: "#b45309" }
14562
14628
  });
14563
14629
 
14564
14630
  // src/widget/screens/ReportSuccessScreen.tsx
@@ -15261,9 +15327,108 @@ var styles13 = import_react_native15.StyleSheet.create({
15261
15327
  fontSize: 14
15262
15328
  }
15263
15329
  });
15330
+
15331
+ // src/BugBearErrorBoundary.tsx
15332
+ var import_react17 = __toESM(require("react"));
15333
+ var import_react_native16 = require("react-native");
15334
+ var BugBearErrorBoundary = class extends import_react17.Component {
15335
+ constructor(props) {
15336
+ super(props);
15337
+ this.reset = () => {
15338
+ this.setState({
15339
+ hasError: false,
15340
+ error: null,
15341
+ errorInfo: null
15342
+ });
15343
+ };
15344
+ this.state = {
15345
+ hasError: false,
15346
+ error: null,
15347
+ errorInfo: null
15348
+ };
15349
+ }
15350
+ static getDerivedStateFromError(error) {
15351
+ return { hasError: true, error };
15352
+ }
15353
+ componentDidCatch(error, errorInfo) {
15354
+ this.setState({ errorInfo });
15355
+ captureError(error, {
15356
+ componentStack: errorInfo.componentStack ?? void 0
15357
+ });
15358
+ console.error("BugBear: Error caught by ErrorBoundary", {
15359
+ error: error.message,
15360
+ componentStack: errorInfo.componentStack?.slice(0, 500)
15361
+ });
15362
+ this.props.onError?.(error, errorInfo);
15363
+ this.props.errorReporter?.(error, {
15364
+ componentStack: errorInfo.componentStack ?? void 0,
15365
+ source: "BugBearErrorBoundary"
15366
+ });
15367
+ }
15368
+ render() {
15369
+ const { hasError, error } = this.state;
15370
+ const { children, fallback } = this.props;
15371
+ if (hasError && error) {
15372
+ if (typeof fallback === "function") {
15373
+ return fallback(error, this.reset);
15374
+ }
15375
+ if (fallback) {
15376
+ return fallback;
15377
+ }
15378
+ 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"));
15379
+ }
15380
+ return children;
15381
+ }
15382
+ };
15383
+ function useErrorContext() {
15384
+ return {
15385
+ captureError,
15386
+ getEnhancedContext: () => contextCapture.getEnhancedContext()
15387
+ };
15388
+ }
15389
+ var styles14 = import_react_native16.StyleSheet.create({
15390
+ container: {
15391
+ padding: 20,
15392
+ margin: 20,
15393
+ backgroundColor: "#fef2f2",
15394
+ borderWidth: 1,
15395
+ borderColor: "#fecaca",
15396
+ borderRadius: 8
15397
+ },
15398
+ title: {
15399
+ fontSize: 16,
15400
+ fontWeight: "600",
15401
+ color: "#991b1b",
15402
+ marginBottom: 8
15403
+ },
15404
+ message: {
15405
+ fontSize: 14,
15406
+ color: "#7f1d1d",
15407
+ marginBottom: 12
15408
+ },
15409
+ button: {
15410
+ backgroundColor: "#dc2626",
15411
+ paddingHorizontal: 16,
15412
+ paddingVertical: 8,
15413
+ borderRadius: 6,
15414
+ alignSelf: "flex-start"
15415
+ },
15416
+ buttonText: {
15417
+ color: "white",
15418
+ fontSize: 14,
15419
+ fontWeight: "500"
15420
+ },
15421
+ caption: {
15422
+ fontSize: 12,
15423
+ color: "#9ca3af",
15424
+ marginTop: 12
15425
+ }
15426
+ });
15264
15427
  // Annotate the CommonJS export names for ESM import in node:
15265
15428
  0 && (module.exports = {
15266
15429
  BugBearButton,
15430
+ BugBearErrorBoundary,
15267
15431
  BugBearProvider,
15268
- useBugBear
15432
+ useBugBear,
15433
+ useErrorContext
15269
15434
  });