@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.mjs CHANGED
@@ -11594,6 +11594,18 @@ 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
+ }
11604
+ var formatPgError = (e) => {
11605
+ if (!e || typeof e !== "object") return { raw: e };
11606
+ const { message, code, details, hint } = e;
11607
+ return { message, code, details, hint };
11608
+ };
11597
11609
  var DEFAULT_SUPABASE_URL = "https://kyxgzjnqgvapvlnvqawz.supabase.co";
11598
11610
  var getEnvVar = (key) => {
11599
11611
  try {
@@ -11761,9 +11773,9 @@ var BugBearClient = class {
11761
11773
  sort_order
11762
11774
  )
11763
11775
  )
11764
- `).eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).in("status", ["pending", "in_progress"]).order("created_at", { ascending: true });
11776
+ `).eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).in("status", ["pending", "in_progress"]).order("created_at", { ascending: true }).limit(100);
11765
11777
  if (error) {
11766
- console.error("BugBear: Failed to fetch assignments", error);
11778
+ console.error("BugBear: Failed to fetch assignments", formatPgError(error));
11767
11779
  return [];
11768
11780
  }
11769
11781
  const mapped = (data || []).map((item) => ({
@@ -11915,7 +11927,7 @@ var BugBearClient = class {
11915
11927
  if (options?.testResult) {
11916
11928
  updateData.test_result = options.testResult;
11917
11929
  }
11918
- const { error } = await this.supabase.from("test_assignments").update(updateData).eq("id", assignmentId);
11930
+ const { data: updatedRow, error } = await this.supabase.from("test_assignments").update(updateData).eq("id", assignmentId).eq("status", currentAssignment.status).select("id").maybeSingle();
11919
11931
  if (error) {
11920
11932
  console.error("BugBear: Failed to update assignment status", {
11921
11933
  message: error.message,
@@ -11928,6 +11940,9 @@ var BugBearClient = class {
11928
11940
  });
11929
11941
  return { success: false, error: error.message };
11930
11942
  }
11943
+ if (!updatedRow) {
11944
+ return { success: false, error: "Assignment status has changed. Please refresh and try again." };
11945
+ }
11931
11946
  if (options?.feedback && ["passed", "failed", "blocked"].includes(status)) {
11932
11947
  const { data: assignmentData, error: fetchError2 } = await this.supabase.from("test_assignments").select("test_case_id").eq("id", assignmentId).single();
11933
11948
  if (fetchError2) {
@@ -11988,7 +12003,7 @@ var BugBearClient = class {
11988
12003
  }
11989
12004
  const { error } = await this.supabase.from("test_assignments").update(updateData).eq("id", assignmentId);
11990
12005
  if (error) {
11991
- console.error("BugBear: Failed to skip assignment", error);
12006
+ console.error("BugBear: Failed to skip assignment", formatPgError(error));
11992
12007
  return { success: false, error: error.message };
11993
12008
  }
11994
12009
  return { success: true };
@@ -12154,6 +12169,28 @@ var BugBearClient = class {
12154
12169
  return null;
12155
12170
  }
12156
12171
  }
12172
+ /**
12173
+ * Get detailed assignment stats for the current tester via RPC.
12174
+ * Returns counts by status (pending, in_progress, passed, failed, blocked, skipped, total).
12175
+ */
12176
+ async getTesterStats() {
12177
+ try {
12178
+ const testerInfo = await this.getTesterInfo();
12179
+ if (!testerInfo) return null;
12180
+ const { data, error } = await this.supabase.rpc("get_tester_stats", {
12181
+ p_project_id: this.config.projectId,
12182
+ p_tester_id: testerInfo.id
12183
+ });
12184
+ if (error) {
12185
+ console.error("BugBear: Failed to fetch tester stats", formatPgError(error));
12186
+ return null;
12187
+ }
12188
+ return data;
12189
+ } catch (err) {
12190
+ console.error("BugBear: Error fetching tester stats", err);
12191
+ return null;
12192
+ }
12193
+ }
12157
12194
  /**
12158
12195
  * Basic email format validation (defense in depth)
12159
12196
  */
@@ -12296,7 +12333,7 @@ var BugBearClient = class {
12296
12333
  if (updates.platforms !== void 0) updateData.platforms = updates.platforms;
12297
12334
  const { error } = await this.supabase.from("testers").update(updateData).eq("id", testerInfo.id);
12298
12335
  if (error) {
12299
- console.error("BugBear: updateTesterProfile error", error);
12336
+ console.error("BugBear: updateTesterProfile error", formatPgError(error));
12300
12337
  return { success: false, error: error.message };
12301
12338
  }
12302
12339
  return { success: true };
@@ -12318,14 +12355,14 @@ var BugBearClient = class {
12318
12355
  */
12319
12356
  async isQAEnabled() {
12320
12357
  try {
12321
- const { data, error } = await this.supabase.from("projects").select("is_qa_enabled").eq("id", this.config.projectId).single();
12358
+ const { data, error } = await this.supabase.rpc("check_qa_enabled", {
12359
+ p_project_id: this.config.projectId
12360
+ });
12322
12361
  if (error) {
12323
- if (error.code !== "PGRST116") {
12324
- console.warn("BugBear: Could not check QA status", error.message || error.code || "Unknown error");
12325
- }
12362
+ console.warn("BugBear: Could not check QA status", error.message || error.code || "Unknown error");
12326
12363
  return true;
12327
12364
  }
12328
- return data?.is_qa_enabled ?? true;
12365
+ return data ?? true;
12329
12366
  } catch (err) {
12330
12367
  return true;
12331
12368
  }
@@ -12355,7 +12392,7 @@ var BugBearClient = class {
12355
12392
  upsert: false
12356
12393
  });
12357
12394
  if (error) {
12358
- console.error("BugBear: Failed to upload screenshot", error);
12395
+ console.error("BugBear: Failed to upload screenshot", formatPgError(error));
12359
12396
  return null;
12360
12397
  }
12361
12398
  const { data: { publicUrl } } = this.supabase.storage.from(bucket).getPublicUrl(path);
@@ -12386,7 +12423,7 @@ var BugBearClient = class {
12386
12423
  upsert: false
12387
12424
  });
12388
12425
  if (error) {
12389
- console.error("BugBear: Failed to upload image from URI", error);
12426
+ console.error("BugBear: Failed to upload image from URI", formatPgError(error));
12390
12427
  return null;
12391
12428
  }
12392
12429
  const { data: { publicUrl } } = this.supabase.storage.from(bucket).getPublicUrl(path);
@@ -12461,7 +12498,7 @@ var BugBearClient = class {
12461
12498
  }
12462
12499
  const { data, error } = await query;
12463
12500
  if (error) {
12464
- console.error("BugBear: Failed to fetch fix requests", error);
12501
+ console.error("BugBear: Failed to fetch fix requests", formatPgError(error));
12465
12502
  return [];
12466
12503
  }
12467
12504
  return (data || []).map((fr) => ({
@@ -12487,75 +12524,35 @@ var BugBearClient = class {
12487
12524
  try {
12488
12525
  const testerInfo = await this.getTesterInfo();
12489
12526
  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 });
12527
+ const { data, error } = await this.supabase.rpc("get_threads_with_unread", {
12528
+ p_project_id: this.config.projectId,
12529
+ p_tester_id: testerInfo.id
12530
+ });
12500
12531
  if (error) {
12501
- console.error("BugBear: Failed to fetch threads", error);
12532
+ console.error("BugBear: Failed to fetch threads via RPC", formatPgError(error));
12502
12533
  return [];
12503
12534
  }
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
- });
12535
+ if (!data || data.length === 0) return [];
12536
+ return data.map((row) => ({
12537
+ id: row.thread_id,
12538
+ subject: row.thread_subject,
12539
+ threadType: row.thread_type,
12540
+ priority: row.thread_priority,
12541
+ isPinned: row.is_pinned,
12542
+ isResolved: row.is_resolved,
12543
+ lastMessageAt: row.last_message_at,
12544
+ createdAt: row.created_at,
12545
+ unreadCount: Number(row.unread_count) || 0,
12546
+ lastMessage: row.last_message_preview ? {
12547
+ id: "",
12548
+ threadId: row.thread_id,
12549
+ senderType: row.last_message_sender_type || "system",
12550
+ senderName: row.last_message_sender_name || "",
12551
+ content: row.last_message_preview,
12552
+ createdAt: row.last_message_at,
12553
+ attachments: []
12554
+ } : void 0
12555
+ }));
12559
12556
  } catch (err) {
12560
12557
  console.error("BugBear: Error fetching threads", err);
12561
12558
  return [];
@@ -12574,9 +12571,9 @@ var BugBearClient = class {
12574
12571
  content,
12575
12572
  created_at,
12576
12573
  attachments
12577
- `).eq("thread_id", threadId).order("created_at", { ascending: true });
12574
+ `).eq("thread_id", threadId).order("created_at", { ascending: true }).limit(200);
12578
12575
  if (error) {
12579
- console.error("BugBear: Failed to fetch messages", error);
12576
+ console.error("BugBear: Failed to fetch messages", formatPgError(error));
12580
12577
  return [];
12581
12578
  }
12582
12579
  return (data || []).map((msg) => ({
@@ -12620,10 +12617,9 @@ var BugBearClient = class {
12620
12617
  }
12621
12618
  const { error } = await this.supabase.from("discussion_messages").insert(insertData);
12622
12619
  if (error) {
12623
- console.error("BugBear: Failed to send message", error);
12620
+ console.error("BugBear: Failed to send message", formatPgError(error));
12624
12621
  return false;
12625
12622
  }
12626
- await this.supabase.from("discussion_threads").update({ last_message_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", threadId);
12627
12623
  await this.markThreadAsRead(threadId);
12628
12624
  return true;
12629
12625
  } catch (err) {
@@ -12737,7 +12733,7 @@ var BugBearClient = class {
12737
12733
  p_platform: options.platform || null
12738
12734
  });
12739
12735
  if (error) {
12740
- console.error("BugBear: Failed to start session", error);
12736
+ console.error("BugBear: Failed to start session", formatPgError(error));
12741
12737
  return { success: false, error: error.message };
12742
12738
  }
12743
12739
  const session = await this.getSession(data);
@@ -12762,7 +12758,7 @@ var BugBearClient = class {
12762
12758
  p_routes_covered: options.routesCovered || null
12763
12759
  });
12764
12760
  if (error) {
12765
- console.error("BugBear: Failed to end session", error);
12761
+ console.error("BugBear: Failed to end session", formatPgError(error));
12766
12762
  return { success: false, error: error.message };
12767
12763
  }
12768
12764
  const session = this.transformSession(data);
@@ -12810,7 +12806,7 @@ var BugBearClient = class {
12810
12806
  if (!testerInfo) return [];
12811
12807
  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);
12812
12808
  if (error) {
12813
- console.error("BugBear: Failed to fetch session history", error);
12809
+ console.error("BugBear: Failed to fetch session history", formatPgError(error));
12814
12810
  return [];
12815
12811
  }
12816
12812
  return (data || []).map((s) => this.transformSession(s));
@@ -12838,7 +12834,7 @@ var BugBearClient = class {
12838
12834
  p_app_context: options.appContext || null
12839
12835
  });
12840
12836
  if (error) {
12841
- console.error("BugBear: Failed to add finding", error);
12837
+ console.error("BugBear: Failed to add finding", formatPgError(error));
12842
12838
  return { success: false, error: error.message };
12843
12839
  }
12844
12840
  const finding = this.transformFinding(data);
@@ -12854,9 +12850,9 @@ var BugBearClient = class {
12854
12850
  */
12855
12851
  async getSessionFindings(sessionId) {
12856
12852
  try {
12857
- const { data, error } = await this.supabase.from("qa_findings").select("*").eq("session_id", sessionId).order("created_at", { ascending: true });
12853
+ const { data, error } = await this.supabase.from("qa_findings").select("*").eq("session_id", sessionId).order("created_at", { ascending: true }).limit(100);
12858
12854
  if (error) {
12859
- console.error("BugBear: Failed to fetch findings", error);
12855
+ console.error("BugBear: Failed to fetch findings", formatPgError(error));
12860
12856
  return [];
12861
12857
  }
12862
12858
  return (data || []).map((f) => this.transformFinding(f));
@@ -12874,7 +12870,7 @@ var BugBearClient = class {
12874
12870
  p_finding_id: findingId
12875
12871
  });
12876
12872
  if (error) {
12877
- console.error("BugBear: Failed to convert finding", error);
12873
+ console.error("BugBear: Failed to convert finding", formatPgError(error));
12878
12874
  return { success: false, error: error.message };
12879
12875
  }
12880
12876
  return { success: true, bugId: data };
@@ -12895,7 +12891,7 @@ var BugBearClient = class {
12895
12891
  dismissed_at: (/* @__PURE__ */ new Date()).toISOString()
12896
12892
  }).eq("id", findingId);
12897
12893
  if (error) {
12898
- console.error("BugBear: Failed to dismiss finding", error);
12894
+ console.error("BugBear: Failed to dismiss finding", formatPgError(error));
12899
12895
  return { success: false, error: error.message };
12900
12896
  }
12901
12897
  return { success: true };
@@ -12998,7 +12994,8 @@ var BugBearContext = createContext({
12998
12994
  updateTesterProfile: async () => ({ success: false }),
12999
12995
  refreshTesterInfo: async () => {
13000
12996
  },
13001
- dashboardUrl: void 0
12997
+ dashboardUrl: void 0,
12998
+ onError: void 0
13002
12999
  });
13003
13000
  function useBugBear() {
13004
13001
  return useContext(BugBearContext);
@@ -13147,6 +13144,9 @@ function BugBearProvider({ config, children, appVersion, enabled = true }) {
13147
13144
  }
13148
13145
  } catch (err) {
13149
13146
  console.error("BugBear: Init error", err);
13147
+ if (err instanceof Error) {
13148
+ config.onError?.(err, { phase: "init" });
13149
+ }
13150
13150
  } finally {
13151
13151
  setIsLoading(false);
13152
13152
  }
@@ -13204,7 +13204,8 @@ function BugBearProvider({ config, children, appVersion, enabled = true }) {
13204
13204
  refreshTesterStatus,
13205
13205
  updateTesterProfile,
13206
13206
  refreshTesterInfo,
13207
- dashboardUrl: config.dashboardUrl
13207
+ dashboardUrl: config.dashboardUrl,
13208
+ onError: config.onError
13208
13209
  }
13209
13210
  },
13210
13211
  children
@@ -13729,6 +13730,7 @@ function TestDetailScreen({ testId, nav }) {
13729
13730
  const [selectedSkipReason, setSelectedSkipReason] = useState2(null);
13730
13731
  const [skipNotes, setSkipNotes] = useState2("");
13731
13732
  const [skipping, setSkipping] = useState2(false);
13733
+ const [isSubmitting, setIsSubmitting] = useState2(false);
13732
13734
  useEffect3(() => {
13733
13735
  setCriteriaResults({});
13734
13736
  setShowSteps(true);
@@ -13751,26 +13753,39 @@ function TestDetailScreen({ testId, nav }) {
13751
13753
  const allTests = assignments;
13752
13754
  const currentIndex = displayedAssignment ? assignments.indexOf(displayedAssignment) : -1;
13753
13755
  const handlePass = useCallback2(async () => {
13754
- if (!client || !displayedAssignment) return;
13756
+ if (!client || !displayedAssignment || isSubmitting) return;
13755
13757
  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]);
13758
+ setIsSubmitting(true);
13759
+ try {
13760
+ await client.passAssignment(displayedAssignment.id);
13761
+ await refreshAssignments();
13762
+ nav.replace({ name: "TEST_FEEDBACK", status: "passed", assignmentId: displayedAssignment.id });
13763
+ } finally {
13764
+ setIsSubmitting(false);
13765
+ }
13766
+ }, [client, displayedAssignment, refreshAssignments, nav, isSubmitting]);
13760
13767
  const handleFail = useCallback2(async () => {
13761
- if (!client || !displayedAssignment) return;
13768
+ if (!client || !displayedAssignment || isSubmitting) return;
13762
13769
  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
13770
+ setIsSubmitting(true);
13771
+ try {
13772
+ const result = await client.failAssignment(displayedAssignment.id);
13773
+ if (!result.success) {
13774
+ console.error("BugBear: Failed to mark assignment as failed", result.error);
13771
13775
  }
13772
- });
13773
- }, [client, displayedAssignment, refreshAssignments, nav]);
13776
+ await refreshAssignments();
13777
+ nav.replace({
13778
+ name: "REPORT",
13779
+ prefill: {
13780
+ type: "test_fail",
13781
+ assignmentId: displayedAssignment.id,
13782
+ testCaseId: displayedAssignment.testCase.id
13783
+ }
13784
+ });
13785
+ } finally {
13786
+ setIsSubmitting(false);
13787
+ }
13788
+ }, [client, displayedAssignment, refreshAssignments, nav, isSubmitting]);
13774
13789
  const handleSkip = useCallback2(async () => {
13775
13790
  if (!client || !displayedAssignment || !selectedSkipReason) return;
13776
13791
  Keyboard.dismiss();
@@ -13851,7 +13866,7 @@ function TestDetailScreen({ testId, nav }) {
13851
13866
  }
13852
13867
  },
13853
13868
  /* @__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:"), [
13869
+ ), /* @__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
13870
  { reason: "blocked", label: "\u{1F6AB} Blocked by a bug" },
13856
13871
  { reason: "not_ready", label: "\u{1F6A7} Feature not ready" },
13857
13872
  { reason: "dependency", label: "\u{1F517} Needs another test first" },
@@ -14421,6 +14436,7 @@ function ReportScreen({ nav, prefill }) {
14421
14436
  const [submitting, setSubmitting] = useState6(false);
14422
14437
  const [error, setError] = useState6(null);
14423
14438
  const images = useImageAttachments(uploadImage, 5, "screenshots");
14439
+ const isRetestFailure = prefill?.type === "test_fail";
14424
14440
  const isBugType = reportType === "bug" || reportType === "test_fail";
14425
14441
  const handleSubmit = async () => {
14426
14442
  if (!client || !description.trim()) return;
@@ -14460,7 +14476,50 @@ function ReportScreen({ nav, prefill }) {
14460
14476
  setSubmitting(false);
14461
14477
  }
14462
14478
  };
14463
- return /* @__PURE__ */ React8.createElement(View7, null, /* @__PURE__ */ React8.createElement(Text7, { style: shared.label }, "What are you reporting?"), /* @__PURE__ */ React8.createElement(View7, { style: styles7.typeRow }, [
14479
+ return /* @__PURE__ */ React8.createElement(View7, null, isRetestFailure ? /* @__PURE__ */ React8.createElement(React8.Fragment, null, /* @__PURE__ */ React8.createElement(View7, { style: styles7.retestBanner }, /* @__PURE__ */ React8.createElement(Text7, { style: styles7.retestIcon }, "\u{1F504}"), /* @__PURE__ */ React8.createElement(View7, null, /* @__PURE__ */ React8.createElement(Text7, { style: styles7.retestTitle }, "Bug Still Present"), /* @__PURE__ */ React8.createElement(Text7, { style: styles7.retestSubtitle }, "The fix did not resolve this issue"))), /* @__PURE__ */ React8.createElement(View7, { style: styles7.section }, /* @__PURE__ */ React8.createElement(Text7, { style: shared.label }, "Severity"), /* @__PURE__ */ React8.createElement(View7, { style: styles7.severityRow }, [
14480
+ { sev: "critical", color: "#ef4444" },
14481
+ { sev: "high", color: "#f97316" },
14482
+ { sev: "medium", color: "#eab308" },
14483
+ { sev: "low", color: "#6b7280" }
14484
+ ].map(({ sev, color }) => /* @__PURE__ */ React8.createElement(
14485
+ TouchableOpacity7,
14486
+ {
14487
+ key: sev,
14488
+ style: [styles7.sevButton, severity === sev && { backgroundColor: `${color}30`, borderColor: color }],
14489
+ onPress: () => setSeverity(sev)
14490
+ },
14491
+ /* @__PURE__ */ React8.createElement(Text7, { style: [styles7.sevText, severity === sev && { color }] }, sev)
14492
+ )))), /* @__PURE__ */ React8.createElement(View7, { style: styles7.section }, /* @__PURE__ */ React8.createElement(Text7, { style: shared.label }, "What went wrong?"), /* @__PURE__ */ React8.createElement(
14493
+ TextInput3,
14494
+ {
14495
+ style: styles7.descInput,
14496
+ value: description,
14497
+ onChangeText: setDescription,
14498
+ placeholder: "Describe what you observed. What still doesn't work?",
14499
+ placeholderTextColor: colors.textMuted,
14500
+ multiline: true,
14501
+ numberOfLines: 4,
14502
+ textAlignVertical: "top"
14503
+ }
14504
+ )), /* @__PURE__ */ React8.createElement(
14505
+ ImagePickerButtons,
14506
+ {
14507
+ images: images.images,
14508
+ maxImages: 5,
14509
+ onPickGallery: images.pickFromGallery,
14510
+ onPickCamera: images.pickFromCamera,
14511
+ onRemove: images.removeImage,
14512
+ label: "Attachments (optional)"
14513
+ }
14514
+ ), error && /* @__PURE__ */ React8.createElement(View7, { style: styles7.errorBanner }, /* @__PURE__ */ React8.createElement(Text7, { style: styles7.errorText }, error)), /* @__PURE__ */ React8.createElement(
14515
+ TouchableOpacity7,
14516
+ {
14517
+ style: [shared.primaryButton, styles7.retestSubmitButton, (!description.trim() || submitting || images.isUploading) && shared.primaryButtonDisabled, { marginTop: 20 }],
14518
+ onPress: handleSubmit,
14519
+ disabled: !description.trim() || submitting || images.isUploading
14520
+ },
14521
+ /* @__PURE__ */ React8.createElement(Text7, { style: shared.primaryButtonText }, images.isUploading ? "Uploading images..." : submitting ? "Submitting..." : error ? "Retry" : "Submit Failed Retest")
14522
+ )) : /* @__PURE__ */ React8.createElement(React8.Fragment, null, /* @__PURE__ */ React8.createElement(Text7, { style: shared.label }, "What are you reporting?"), /* @__PURE__ */ React8.createElement(View7, { style: styles7.typeRow }, [
14464
14523
  { type: "bug", label: "Bug", icon: "\u{1F41B}" },
14465
14524
  { type: "feedback", label: "Feedback", icon: "\u{1F4A1}" },
14466
14525
  { type: "suggestion", label: "Idea", icon: "\u2728" }
@@ -14525,7 +14584,7 @@ function ReportScreen({ nav, prefill }) {
14525
14584
  disabled: !description.trim() || submitting || images.isUploading
14526
14585
  },
14527
14586
  /* @__PURE__ */ React8.createElement(Text7, { style: shared.primaryButtonText }, images.isUploading ? "Uploading images..." : submitting ? "Submitting..." : error ? "Retry" : "Submit Report")
14528
- ));
14587
+ )));
14529
14588
  }
14530
14589
  var styles7 = StyleSheet8.create({
14531
14590
  typeRow: { flexDirection: "row", gap: 10, marginBottom: 20 },
@@ -14542,7 +14601,12 @@ var styles7 = StyleSheet8.create({
14542
14601
  screenInput: { backgroundColor: colors.card, borderWidth: 1, borderColor: colors.border, borderRadius: 8, paddingHorizontal: 12, paddingVertical: 8, fontSize: 13, color: colors.textPrimary },
14543
14602
  screenHint: { fontSize: 11, color: colors.textMuted, marginTop: 4 },
14544
14603
  errorBanner: { backgroundColor: "#7f1d1d", borderWidth: 1, borderColor: "#991b1b", borderRadius: 8, padding: 12, marginTop: 16 },
14545
- errorText: { fontSize: 13, color: "#fca5a5", lineHeight: 18 }
14604
+ errorText: { fontSize: 13, color: "#fca5a5", lineHeight: 18 },
14605
+ retestBanner: { flexDirection: "row", alignItems: "center", gap: 10, backgroundColor: "#422006", borderWidth: 1, borderColor: "#854d0e", borderRadius: 10, paddingVertical: 12, paddingHorizontal: 14, marginBottom: 20 },
14606
+ retestIcon: { fontSize: 16 },
14607
+ retestTitle: { fontSize: 15, fontWeight: "600", color: "#fbbf24", lineHeight: 20 },
14608
+ retestSubtitle: { fontSize: 12, color: "#d97706", lineHeight: 16 },
14609
+ retestSubmitButton: { backgroundColor: "#b45309" }
14546
14610
  });
14547
14611
 
14548
14612
  // src/widget/screens/ReportSuccessScreen.tsx
@@ -15245,8 +15309,107 @@ var styles13 = StyleSheet14.create({
15245
15309
  fontSize: 14
15246
15310
  }
15247
15311
  });
15312
+
15313
+ // src/BugBearErrorBoundary.tsx
15314
+ import React15, { Component } from "react";
15315
+ import { View as View14, Text as Text14, TouchableOpacity as TouchableOpacity13, StyleSheet as StyleSheet15 } from "react-native";
15316
+ var BugBearErrorBoundary = class extends Component {
15317
+ constructor(props) {
15318
+ super(props);
15319
+ this.reset = () => {
15320
+ this.setState({
15321
+ hasError: false,
15322
+ error: null,
15323
+ errorInfo: null
15324
+ });
15325
+ };
15326
+ this.state = {
15327
+ hasError: false,
15328
+ error: null,
15329
+ errorInfo: null
15330
+ };
15331
+ }
15332
+ static getDerivedStateFromError(error) {
15333
+ return { hasError: true, error };
15334
+ }
15335
+ componentDidCatch(error, errorInfo) {
15336
+ this.setState({ errorInfo });
15337
+ captureError(error, {
15338
+ componentStack: errorInfo.componentStack ?? void 0
15339
+ });
15340
+ console.error("BugBear: Error caught by ErrorBoundary", {
15341
+ error: error.message,
15342
+ componentStack: errorInfo.componentStack?.slice(0, 500)
15343
+ });
15344
+ this.props.onError?.(error, errorInfo);
15345
+ this.props.errorReporter?.(error, {
15346
+ componentStack: errorInfo.componentStack ?? void 0,
15347
+ source: "BugBearErrorBoundary"
15348
+ });
15349
+ }
15350
+ render() {
15351
+ const { hasError, error } = this.state;
15352
+ const { children, fallback } = this.props;
15353
+ if (hasError && error) {
15354
+ if (typeof fallback === "function") {
15355
+ return fallback(error, this.reset);
15356
+ }
15357
+ if (fallback) {
15358
+ return fallback;
15359
+ }
15360
+ 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"));
15361
+ }
15362
+ return children;
15363
+ }
15364
+ };
15365
+ function useErrorContext() {
15366
+ return {
15367
+ captureError,
15368
+ getEnhancedContext: () => contextCapture.getEnhancedContext()
15369
+ };
15370
+ }
15371
+ var styles14 = StyleSheet15.create({
15372
+ container: {
15373
+ padding: 20,
15374
+ margin: 20,
15375
+ backgroundColor: "#fef2f2",
15376
+ borderWidth: 1,
15377
+ borderColor: "#fecaca",
15378
+ borderRadius: 8
15379
+ },
15380
+ title: {
15381
+ fontSize: 16,
15382
+ fontWeight: "600",
15383
+ color: "#991b1b",
15384
+ marginBottom: 8
15385
+ },
15386
+ message: {
15387
+ fontSize: 14,
15388
+ color: "#7f1d1d",
15389
+ marginBottom: 12
15390
+ },
15391
+ button: {
15392
+ backgroundColor: "#dc2626",
15393
+ paddingHorizontal: 16,
15394
+ paddingVertical: 8,
15395
+ borderRadius: 6,
15396
+ alignSelf: "flex-start"
15397
+ },
15398
+ buttonText: {
15399
+ color: "white",
15400
+ fontSize: 14,
15401
+ fontWeight: "500"
15402
+ },
15403
+ caption: {
15404
+ fontSize: 12,
15405
+ color: "#9ca3af",
15406
+ marginTop: 12
15407
+ }
15408
+ });
15248
15409
  export {
15249
15410
  BugBearButton,
15411
+ BugBearErrorBoundary,
15250
15412
  BugBearProvider,
15251
- useBugBear
15413
+ useBugBear,
15414
+ useErrorContext
15252
15415
  };
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.1",
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",