@bbearai/react-native 0.8.2 → 0.8.3

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
@@ -79,6 +79,11 @@ interface BugBearContextValue {
79
79
  refreshTesterInfo: () => Promise<void>;
80
80
  issueCounts: IssueCounts;
81
81
  refreshIssueCounts: () => Promise<void>;
82
+ /** Reopen a done issue with a required reason */
83
+ reopenReport: (reportId: string, reason: string) => Promise<{
84
+ success: boolean;
85
+ error?: string;
86
+ }>;
82
87
  /** Number of queued offline operations waiting to be sent. */
83
88
  queuedCount: number;
84
89
  /** URL to the BugBear web dashboard (for linking testers to the full web experience) */
package/dist/index.d.ts CHANGED
@@ -79,6 +79,11 @@ interface BugBearContextValue {
79
79
  refreshTesterInfo: () => Promise<void>;
80
80
  issueCounts: IssueCounts;
81
81
  refreshIssueCounts: () => Promise<void>;
82
+ /** Reopen a done issue with a required reason */
83
+ reopenReport: (reportId: string, reason: string) => Promise<{
84
+ success: boolean;
85
+ error?: string;
86
+ }>;
82
87
  /** Number of queued offline operations waiting to be sent. */
83
88
  queuedCount: number;
84
89
  /** URL to the BugBear web dashboard (for linking testers to the full web experience) */
package/dist/index.js CHANGED
@@ -12230,6 +12230,7 @@ var BugBearClient = class {
12230
12230
  this.navigationHistory = [];
12231
12231
  this.reportSubmitInFlight = false;
12232
12232
  this._queue = null;
12233
+ this._sessionStorage = new LocalStorageAdapter();
12233
12234
  this.realtimeChannels = [];
12234
12235
  this.monitor = null;
12235
12236
  this.initialized = false;
@@ -12307,6 +12308,10 @@ var BugBearClient = class {
12307
12308
  this.initOfflineQueue();
12308
12309
  this.initMonitoring();
12309
12310
  }
12311
+ /** Cache key scoped to the active project. */
12312
+ get sessionCacheKey() {
12313
+ return `bugbear_session_${this.config.projectId ?? "unknown"}`;
12314
+ }
12310
12315
  /** Initialize offline queue if configured. Shared by both init paths. */
12311
12316
  initOfflineQueue() {
12312
12317
  if (this.config.offlineQueue?.enabled) {
@@ -12370,6 +12375,26 @@ var BugBearClient = class {
12370
12375
  await this.pendingInit;
12371
12376
  if (this.initError) throw this.initError;
12372
12377
  }
12378
+ /**
12379
+ * Fire-and-forget call to a dashboard notification endpoint.
12380
+ * Only works when apiKey is configured (needed for API auth).
12381
+ * Failures are silently ignored — notifications are best-effort.
12382
+ */
12383
+ async notifyDashboard(path, body) {
12384
+ if (!this.config.apiKey) return;
12385
+ try {
12386
+ const baseUrl = (this.config.apiBaseUrl || DEFAULT_API_BASE_URL).replace(/\/$/, "");
12387
+ await fetch(`${baseUrl}/api/v1/notifications/${path}`, {
12388
+ method: "POST",
12389
+ headers: {
12390
+ "Content-Type": "application/json",
12391
+ "Authorization": `Bearer ${this.config.apiKey}`
12392
+ },
12393
+ body: JSON.stringify(body)
12394
+ });
12395
+ } catch {
12396
+ }
12397
+ }
12373
12398
  // ── Offline Queue ─────────────────────────────────────────
12374
12399
  /**
12375
12400
  * Access the offline queue (if enabled).
@@ -12396,6 +12421,48 @@ var BugBearClient = class {
12396
12421
  }
12397
12422
  await this._queue.load();
12398
12423
  }
12424
+ // ── Session Cache ──────────────────────────────────────────
12425
+ /**
12426
+ * Swap the session cache storage adapter (for React Native — pass AsyncStorage).
12427
+ * Must be called before getCachedSession() for persistence across app kills.
12428
+ * Web callers don't need this — LocalStorageAdapter is the default.
12429
+ */
12430
+ setSessionStorage(adapter) {
12431
+ this._sessionStorage = adapter;
12432
+ }
12433
+ /**
12434
+ * Cache the active QA session locally for instant restore on app restart.
12435
+ * Pass null to clear the cache (e.g. after ending a session).
12436
+ */
12437
+ async cacheSession(session) {
12438
+ try {
12439
+ if (session) {
12440
+ await this._sessionStorage.setItem(this.sessionCacheKey, JSON.stringify(session));
12441
+ } else {
12442
+ await this._sessionStorage.removeItem(this.sessionCacheKey);
12443
+ }
12444
+ } catch {
12445
+ }
12446
+ }
12447
+ /**
12448
+ * Retrieve the cached QA session. Returns null if no cache, if stale (>24h),
12449
+ * or if parsing fails. The DB fetch in initializeBugBear() is the source of truth.
12450
+ */
12451
+ async getCachedSession() {
12452
+ try {
12453
+ const raw = await this._sessionStorage.getItem(this.sessionCacheKey);
12454
+ if (!raw) return null;
12455
+ const session = JSON.parse(raw);
12456
+ const age = Date.now() - new Date(session.startedAt).getTime();
12457
+ if (age > 24 * 60 * 60 * 1e3) {
12458
+ await this._sessionStorage.removeItem(this.sessionCacheKey);
12459
+ return null;
12460
+ }
12461
+ return session;
12462
+ } catch {
12463
+ return null;
12464
+ }
12465
+ }
12399
12466
  registerQueueHandlers() {
12400
12467
  if (!this._queue) return;
12401
12468
  this._queue.registerHandler("report", async (payload) => {
@@ -12413,6 +12480,11 @@ var BugBearClient = class {
12413
12480
  if (error) return { success: false, error: error.message };
12414
12481
  return { success: true };
12415
12482
  });
12483
+ this._queue.registerHandler("email_capture", async (payload) => {
12484
+ const { error } = await this.supabase.from("email_captures").insert(payload).select("id").single();
12485
+ if (error) return { success: false, error: error.message };
12486
+ return { success: true };
12487
+ });
12416
12488
  }
12417
12489
  // ── Realtime Subscriptions ─────────────────────────────────
12418
12490
  /** Whether realtime is enabled in config. */
@@ -12600,6 +12672,8 @@ var BugBearClient = class {
12600
12672
  if (this.config.onReportSubmitted) {
12601
12673
  this.config.onReportSubmitted(report);
12602
12674
  }
12675
+ this.notifyDashboard("report", { reportId: data.id }).catch(() => {
12676
+ });
12603
12677
  return { success: true, reportId: data.id };
12604
12678
  } catch (err) {
12605
12679
  const message = err instanceof Error ? err.message : "Unknown error";
@@ -12612,6 +12686,44 @@ var BugBearClient = class {
12612
12686
  this.reportSubmitInFlight = false;
12613
12687
  }
12614
12688
  }
12689
+ /**
12690
+ * Capture an email for QA testing.
12691
+ * Called by the email interceptor — not typically called directly.
12692
+ */
12693
+ async captureEmail(payload) {
12694
+ try {
12695
+ await this.ready();
12696
+ if (!payload.subject || !payload.to || payload.to.length === 0) {
12697
+ return { success: false, error: "subject and to are required" };
12698
+ }
12699
+ const record = {
12700
+ project_id: this.config.projectId,
12701
+ to_addresses: payload.to,
12702
+ from_address: payload.from || null,
12703
+ subject: payload.subject,
12704
+ html_content: payload.html || null,
12705
+ text_content: payload.text || null,
12706
+ template_id: payload.templateId || null,
12707
+ metadata: payload.metadata || {},
12708
+ capture_mode: payload.captureMode,
12709
+ was_delivered: payload.wasDelivered,
12710
+ delivery_status: payload.wasDelivered ? "sent" : "pending"
12711
+ };
12712
+ const { data, error } = await this.supabase.from("email_captures").insert(record).select("id").single();
12713
+ if (error) {
12714
+ if (this._queue && isNetworkError(error.message)) {
12715
+ await this._queue.enqueue("email_capture", record);
12716
+ return { success: false, queued: true, error: "Queued \u2014 will send when online" };
12717
+ }
12718
+ console.error("BugBear: Failed to capture email", error.message);
12719
+ return { success: false, error: error.message };
12720
+ }
12721
+ return { success: true, captureId: data.id };
12722
+ } catch (err) {
12723
+ const message = err instanceof Error ? err.message : "Unknown error";
12724
+ return { success: false, error: message };
12725
+ }
12726
+ }
12615
12727
  /**
12616
12728
  * Get assigned tests for current user
12617
12729
  * First looks up the tester by email, then fetches their assignments
@@ -13233,6 +13345,32 @@ var BugBearClient = class {
13233
13345
  return [];
13234
13346
  }
13235
13347
  }
13348
+ /**
13349
+ * Reopen a done issue that the tester believes isn't actually fixed.
13350
+ * Transitions the report from a done status back to 'confirmed'.
13351
+ */
13352
+ async reopenReport(reportId, reason) {
13353
+ try {
13354
+ const testerInfo = await this.getTesterInfo();
13355
+ if (!testerInfo) return { success: false, error: "Not authenticated as tester" };
13356
+ const { data, error } = await this.supabase.rpc("reopen_report", {
13357
+ p_report_id: reportId,
13358
+ p_tester_id: testerInfo.id,
13359
+ p_reason: reason
13360
+ });
13361
+ if (error) {
13362
+ console.error("BugBear: Failed to reopen report", formatPgError(error));
13363
+ return { success: false, error: error.message };
13364
+ }
13365
+ if (!data?.success) {
13366
+ return { success: false, error: data?.error || "Failed to reopen report" };
13367
+ }
13368
+ return { success: true };
13369
+ } catch (err) {
13370
+ console.error("BugBear: Error reopening report", err);
13371
+ return { success: false, error: "Unexpected error" };
13372
+ }
13373
+ }
13236
13374
  /**
13237
13375
  * Basic email format validation (defense in depth)
13238
13376
  */
@@ -13819,7 +13957,7 @@ var BugBearClient = class {
13819
13957
  insertData.attachments = safeAttachments;
13820
13958
  }
13821
13959
  }
13822
- const { error } = await this.supabase.from("discussion_messages").insert(insertData);
13960
+ const { data: msgData, error } = await this.supabase.from("discussion_messages").insert(insertData).select("id").single();
13823
13961
  if (error) {
13824
13962
  if (this._queue && isNetworkError(error.message)) {
13825
13963
  await this._queue.enqueue("message", insertData);
@@ -13828,6 +13966,10 @@ var BugBearClient = class {
13828
13966
  console.error("BugBear: Failed to send message", formatPgError(error));
13829
13967
  return false;
13830
13968
  }
13969
+ if (msgData?.id) {
13970
+ this.notifyDashboard("message", { threadId, messageId: msgData.id }).catch(() => {
13971
+ });
13972
+ }
13831
13973
  await this.markThreadAsRead(threadId);
13832
13974
  return true;
13833
13975
  } catch (err) {
@@ -13953,6 +14095,7 @@ var BugBearClient = class {
13953
14095
  if (!session) {
13954
14096
  return { success: false, error: "Session created but could not be fetched" };
13955
14097
  }
14098
+ await this.cacheSession(session);
13956
14099
  return { success: true, session };
13957
14100
  } catch (err) {
13958
14101
  const message = err instanceof Error ? err.message : "Unknown error";
@@ -13976,6 +14119,7 @@ var BugBearClient = class {
13976
14119
  return { success: false, error: error.message };
13977
14120
  }
13978
14121
  const session = this.transformSession(data);
14122
+ await this.cacheSession(null);
13979
14123
  return { success: true, session };
13980
14124
  } catch (err) {
13981
14125
  const message = err instanceof Error ? err.message : "Unknown error";
@@ -13991,8 +14135,13 @@ var BugBearClient = class {
13991
14135
  const testerInfo = await this.getTesterInfo();
13992
14136
  if (!testerInfo) return null;
13993
14137
  const { data, error } = await this.supabase.from("qa_sessions").select("*").eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).eq("status", "active").order("started_at", { ascending: false }).limit(1).maybeSingle();
13994
- if (error || !data) return null;
13995
- return this.transformSession(data);
14138
+ if (error || !data) {
14139
+ await this.cacheSession(null);
14140
+ return null;
14141
+ }
14142
+ const session = this.transformSession(data);
14143
+ await this.cacheSession(session);
14144
+ return session;
13996
14145
  } catch (err) {
13997
14146
  console.error("BugBear: Error fetching active session", err);
13998
14147
  return null;
@@ -14510,6 +14659,7 @@ var BugBearContext = (0, import_react.createContext)({
14510
14659
  issueCounts: { open: 0, done: 0, reopened: 0 },
14511
14660
  refreshIssueCounts: async () => {
14512
14661
  },
14662
+ reopenReport: async () => ({ success: false }),
14513
14663
  queuedCount: 0,
14514
14664
  dashboardUrl: void 0,
14515
14665
  onError: void 0
@@ -14651,6 +14801,14 @@ function BugBearProvider({ config, children, appVersion, enabled = true }) {
14651
14801
  const counts = await client.getIssueCounts();
14652
14802
  setIssueCounts(counts);
14653
14803
  }, [client]);
14804
+ const reopenReport = (0, import_react.useCallback)(async (reportId, reason) => {
14805
+ if (!client) return { success: false, error: "Client not initialized" };
14806
+ const result = await client.reopenReport(reportId, reason);
14807
+ if (result.success) {
14808
+ await refreshIssueCounts();
14809
+ }
14810
+ return result;
14811
+ }, [client, refreshIssueCounts]);
14654
14812
  const initializeBugBear = (0, import_react.useCallback)(async (bugBearClient) => {
14655
14813
  setIsLoading(true);
14656
14814
  try {
@@ -14736,6 +14894,10 @@ function BugBearProvider({ config, children, appVersion, enabled = true }) {
14736
14894
  setClient(newClient);
14737
14895
  (async () => {
14738
14896
  try {
14897
+ const cachedSession = await newClient.getCachedSession();
14898
+ if (cachedSession) {
14899
+ setActiveSession(cachedSession);
14900
+ }
14739
14901
  await initializeBugBear(newClient);
14740
14902
  if (newClient.monitor && config.monitoring) {
14741
14903
  const getCurrentRoute = () => contextCapture.getCurrentRoute();
@@ -14870,6 +15032,7 @@ function BugBearProvider({ config, children, appVersion, enabled = true }) {
14870
15032
  // Issue tracking
14871
15033
  issueCounts,
14872
15034
  refreshIssueCounts,
15035
+ reopenReport,
14873
15036
  queuedCount,
14874
15037
  dashboardUrl: config.dashboardUrl,
14875
15038
  onError: config.onError
@@ -15601,6 +15764,17 @@ function TestDetailScreen({ testId, nav }) {
15601
15764
  setShowSteps(true);
15602
15765
  setShowDetails(false);
15603
15766
  }, [displayedAssignment?.id]);
15767
+ (0, import_react5.useEffect)(() => {
15768
+ if (!client || !displayedAssignment || displayedAssignment.status !== "pending") return;
15769
+ let cancelled = false;
15770
+ (async () => {
15771
+ await client.updateAssignmentStatus(displayedAssignment.id, "in_progress");
15772
+ if (!cancelled) await refreshAssignments();
15773
+ })();
15774
+ return () => {
15775
+ cancelled = true;
15776
+ };
15777
+ }, [client, displayedAssignment?.id, displayedAssignment?.status, refreshAssignments]);
15604
15778
  (0, import_react5.useEffect)(() => {
15605
15779
  const active = displayedAssignment?.status === "in_progress" ? displayedAssignment : null;
15606
15780
  if (!active?.startedAt) {
@@ -15651,17 +15825,6 @@ function TestDetailScreen({ testId, nav }) {
15651
15825
  setIsSubmitting(false);
15652
15826
  }
15653
15827
  }, [client, displayedAssignment, refreshAssignments, nav, isSubmitting]);
15654
- const handleStart = (0, import_react5.useCallback)(async () => {
15655
- if (!client || !displayedAssignment || isSubmitting) return;
15656
- import_react_native5.Keyboard.dismiss();
15657
- setIsSubmitting(true);
15658
- try {
15659
- await client.updateAssignmentStatus(displayedAssignment.id, "in_progress");
15660
- await refreshAssignments();
15661
- } finally {
15662
- setIsSubmitting(false);
15663
- }
15664
- }, [client, displayedAssignment, refreshAssignments, isSubmitting]);
15665
15828
  const handleReopen = (0, import_react5.useCallback)(async () => {
15666
15829
  if (!client || !displayedAssignment || isSubmitting) return;
15667
15830
  import_react_native5.Keyboard.dismiss();
@@ -15807,15 +15970,7 @@ function TestDetailScreen({ testId, nav }) {
15807
15970
  disabled: isSubmitting
15808
15971
  },
15809
15972
  /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles5.passBtnText }, "Change to Pass")
15810
- ))) : displayedAssignment.status === "pending" ? /* @__PURE__ */ import_react5.default.createElement(
15811
- import_react_native5.TouchableOpacity,
15812
- {
15813
- style: [styles5.startBtn, isSubmitting && { opacity: 0.5 }],
15814
- onPress: handleStart,
15815
- disabled: isSubmitting
15816
- },
15817
- /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles5.startBtnText }, isSubmitting ? "Starting..." : displayedAssignment.isVerification ? "\u{1F50D} Start Verification" : "\u25B6 Start Test")
15818
- ) : /* @__PURE__ */ import_react5.default.createElement(import_react_native5.View, { style: styles5.actionButtons }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.TouchableOpacity, { style: [styles5.actionBtn, styles5.failBtn, isSubmitting && { opacity: 0.5 }], onPress: handleFail, disabled: isSubmitting }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles5.failBtnText }, isSubmitting ? displayedAssignment.isVerification ? "Reporting..." : "Failing..." : displayedAssignment.isVerification ? "\u2717 Still Broken" : "Fail")), /* @__PURE__ */ import_react5.default.createElement(import_react_native5.TouchableOpacity, { style: [styles5.actionBtn, styles5.skipBtn, isSubmitting && { opacity: 0.5 }], onPress: () => setShowSkipModal(true), disabled: isSubmitting }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles5.skipBtnText }, "Skip")), /* @__PURE__ */ import_react5.default.createElement(import_react_native5.TouchableOpacity, { style: [styles5.actionBtn, styles5.passBtn, isSubmitting && { opacity: 0.5 }], onPress: handlePass, disabled: isSubmitting }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles5.passBtnText }, isSubmitting ? displayedAssignment.isVerification ? "Verifying..." : "Passing..." : displayedAssignment.isVerification ? "\u2713 Fix Verified" : "Pass"))), /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Modal, { visible: showSkipModal, transparent: true, animationType: "fade" }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.View, { style: styles5.modalOverlay }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.View, { style: styles5.modalContent }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles5.modalTitle }, "Skip this test?"), /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles5.modalSubtitle }, "Select a reason:"), [
15973
+ ))) : /* @__PURE__ */ import_react5.default.createElement(import_react_native5.View, { style: styles5.actionButtons }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.TouchableOpacity, { style: [styles5.actionBtn, styles5.failBtn, isSubmitting && { opacity: 0.5 }], onPress: handleFail, disabled: isSubmitting }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles5.failBtnText }, isSubmitting ? displayedAssignment.isVerification ? "Reporting..." : "Failing..." : displayedAssignment.isVerification ? "\u2717 Still Broken" : "Fail")), /* @__PURE__ */ import_react5.default.createElement(import_react_native5.TouchableOpacity, { style: [styles5.actionBtn, styles5.skipBtn, isSubmitting && { opacity: 0.5 }], onPress: () => setShowSkipModal(true), disabled: isSubmitting }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles5.skipBtnText }, "Skip")), /* @__PURE__ */ import_react5.default.createElement(import_react_native5.TouchableOpacity, { style: [styles5.actionBtn, styles5.passBtn, isSubmitting && { opacity: 0.5 }], onPress: handlePass, disabled: isSubmitting }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles5.passBtnText }, isSubmitting ? displayedAssignment.isVerification ? "Verifying..." : "Passing..." : displayedAssignment.isVerification ? "\u2713 Fix Verified" : "Pass"))), /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Modal, { visible: showSkipModal, transparent: true, animationType: "fade" }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.View, { style: styles5.modalOverlay }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.View, { style: styles5.modalContent }, /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles5.modalTitle }, "Skip this test?"), /* @__PURE__ */ import_react5.default.createElement(import_react_native5.Text, { style: styles5.modalSubtitle }, "Select a reason:"), [
15819
15974
  { reason: "blocked", label: "\u{1F6AB} Blocked by a bug" },
15820
15975
  { reason: "not_ready", label: "\u{1F6A7} Feature not ready" },
15821
15976
  { reason: "dependency", label: "\u{1F517} Needs another test first" },
@@ -15929,9 +16084,6 @@ function createStyles2() {
15929
16084
  completedLabel: { fontSize: 13, fontWeight: "600", color: colors.textSecondary },
15930
16085
  reopenBtn: { backgroundColor: colors.card, borderWidth: 1, borderColor: colors.blue },
15931
16086
  reopenBtnText: { fontSize: 14, fontWeight: "600", color: colors.blue },
15932
- // Start Test button
15933
- startBtn: { paddingVertical: 16, borderRadius: 12, alignItems: "center", backgroundColor: colors.blueSurface, borderWidth: 1, borderColor: colors.blueAccent, marginTop: 8 },
15934
- startBtnText: { fontSize: 16, fontWeight: "600", color: colors.blueText },
15935
16087
  // Action buttons
15936
16088
  actionButtons: { flexDirection: "row", gap: 10, marginTop: 8 },
15937
16089
  actionBtn: { flex: 1, paddingVertical: 14, borderRadius: 12, alignItems: "center" },
@@ -17685,9 +17837,15 @@ function createStyles11() {
17685
17837
  // src/widget/screens/IssueDetailScreen.tsx
17686
17838
  var import_react19 = __toESM(require("react"));
17687
17839
  var import_react_native18 = require("react-native");
17840
+ var DONE_STATUSES = ["verified", "resolved", "closed", "reviewed"];
17688
17841
  function IssueDetailScreen({ nav, issue }) {
17689
- const { dashboardUrl, widgetColorScheme } = useBugBear();
17842
+ const { dashboardUrl, widgetColorScheme, reopenReport } = useBugBear();
17690
17843
  const styles5 = (0, import_react19.useMemo)(() => createStyles12(), [widgetColorScheme]);
17844
+ const [showReopenForm, setShowReopenForm] = (0, import_react19.useState)(false);
17845
+ const [reopenReason, setReopenReason] = (0, import_react19.useState)("");
17846
+ const [isSubmitting, setIsSubmitting] = (0, import_react19.useState)(false);
17847
+ const [reopenError, setReopenError] = (0, import_react19.useState)(null);
17848
+ const [wasReopened, setWasReopened] = (0, import_react19.useState)(false);
17691
17849
  const STATUS_LABELS = (0, import_react19.useMemo)(() => ({
17692
17850
  new: { label: "New", bg: colors.blueDark, color: colors.blueLight },
17693
17851
  triaging: { label: "Triaging", bg: colors.blueDark, color: colors.blueLight },
@@ -17710,7 +17868,68 @@ function IssueDetailScreen({ nav, issue }) {
17710
17868
  }), [widgetColorScheme]);
17711
17869
  const statusConfig = STATUS_LABELS[issue.status] || { label: issue.status, bg: colors.card, color: colors.textSecondary };
17712
17870
  const severityConfig = issue.severity ? SEVERITY_CONFIG[issue.severity] : null;
17713
- return /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, null, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.badgeRow }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: [styles5.badge, { backgroundColor: statusConfig.bg }] }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: [styles5.badgeText, { color: statusConfig.color }] }, statusConfig.label)), severityConfig && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: [styles5.badge, { backgroundColor: severityConfig.bg }] }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: [styles5.badgeText, { color: severityConfig.color }] }, severityConfig.label))), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.title }, issue.title), issue.route && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.route }, issue.route), issue.description && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.descriptionCard }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.descriptionText }, issue.description)), issue.verifiedByName && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.verifiedCard }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.verifiedHeader }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.verifiedIcon }, "\u2705"), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.verifiedTitle }, "Retesting Proof")), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.verifiedBody }, "Verified by ", issue.verifiedByName, issue.verifiedAt && ` on ${new Date(issue.verifiedAt).toLocaleDateString(void 0, { month: "short", day: "numeric", year: "numeric" })}`)), issue.originalBugTitle && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.originalBugCard }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.originalBugHeader }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.originalBugIcon }, "\u{1F504}"), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.originalBugTitle }, "Original Bug")), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.originalBugBody }, "Retest of: ", issue.originalBugTitle)), issue.screenshotUrls && issue.screenshotUrls.length > 0 && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.screenshotSection }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.screenshotLabel }, "Screenshots (", issue.screenshotUrls.length, ")"), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.screenshotRow }, issue.screenshotUrls.map((url, i) => /* @__PURE__ */ import_react19.default.createElement(import_react_native18.TouchableOpacity, { key: i, onPress: () => import_react_native18.Linking.openURL(url), activeOpacity: 0.7 }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Image, { source: { uri: url }, style: styles5.screenshotThumb }))))), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.metaSection }, issue.reporterName && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.metaText }, "Reported by ", issue.reporterName), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.metaTextSmall }, "Created ", formatRelativeTime(issue.createdAt), " ", "\xB7", " Updated ", formatRelativeTime(issue.updatedAt))), dashboardUrl && /* @__PURE__ */ import_react19.default.createElement(
17871
+ const isDone = DONE_STATUSES.includes(issue.status);
17872
+ const handleReopen = (0, import_react19.useCallback)(async () => {
17873
+ if (!reopenReason.trim()) return;
17874
+ setIsSubmitting(true);
17875
+ setReopenError(null);
17876
+ const result = await reopenReport(issue.id, reopenReason.trim());
17877
+ setIsSubmitting(false);
17878
+ if (result.success) {
17879
+ setWasReopened(true);
17880
+ setShowReopenForm(false);
17881
+ } else {
17882
+ setReopenError(result.error || "Failed to reopen");
17883
+ }
17884
+ }, [reopenReason, reopenReport, issue.id]);
17885
+ return /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, null, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.badgeRow }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: [styles5.badge, { backgroundColor: wasReopened ? colors.yellowDark : statusConfig.bg }] }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: [styles5.badgeText, { color: wasReopened ? colors.yellowLight : statusConfig.color }] }, wasReopened ? "Reopened" : statusConfig.label)), severityConfig && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: [styles5.badge, { backgroundColor: severityConfig.bg }] }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: [styles5.badgeText, { color: severityConfig.color }] }, severityConfig.label))), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.title }, issue.title), issue.route && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.route }, issue.route), issue.description && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.descriptionCard }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.descriptionText }, issue.description)), wasReopened && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.reopenedCard }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.reopenedRow }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.reopenedIcon }, "\u{1F504}"), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.reopenedText }, "Issue reopened \u2014 your team has been notified"))), issue.verifiedByName && !wasReopened && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.verifiedCard }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.verifiedHeader }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.verifiedIcon }, "\u2705"), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.verifiedTitle }, "Retesting Proof")), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.verifiedBody }, "Verified by ", issue.verifiedByName, issue.verifiedAt && ` on ${new Date(issue.verifiedAt).toLocaleDateString(void 0, { month: "short", day: "numeric", year: "numeric" })}`)), issue.originalBugTitle && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.originalBugCard }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.originalBugHeader }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.originalBugIcon }, "\u{1F504}"), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.originalBugTitleText }, "Original Bug")), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.originalBugBody }, "Retest of: ", issue.originalBugTitle)), isDone && !wasReopened && !showReopenForm && /* @__PURE__ */ import_react19.default.createElement(
17886
+ import_react_native18.TouchableOpacity,
17887
+ {
17888
+ style: styles5.reopenButton,
17889
+ onPress: () => setShowReopenForm(true),
17890
+ activeOpacity: 0.7
17891
+ },
17892
+ /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.reopenButtonText }, "\u{1F504}", " Not Fixed \u2014 Reopen Issue")
17893
+ ), showReopenForm && !wasReopened && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.reopenForm }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.reopenFormTitle }, "Why isn't this fixed?"), /* @__PURE__ */ import_react19.default.createElement(
17894
+ import_react_native18.TextInput,
17895
+ {
17896
+ value: reopenReason,
17897
+ onChangeText: setReopenReason,
17898
+ placeholder: "Describe what you're still seeing...",
17899
+ placeholderTextColor: colors.textMuted,
17900
+ style: styles5.reopenInput,
17901
+ multiline: true,
17902
+ autoFocus: true
17903
+ }
17904
+ ), reopenError && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.reopenErrorText }, reopenError), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.reopenActions }, /* @__PURE__ */ import_react19.default.createElement(
17905
+ import_react_native18.TouchableOpacity,
17906
+ {
17907
+ style: [
17908
+ styles5.reopenSubmitButton,
17909
+ (!reopenReason.trim() || isSubmitting) && styles5.reopenSubmitDisabled
17910
+ ],
17911
+ onPress: handleReopen,
17912
+ disabled: isSubmitting || !reopenReason.trim(),
17913
+ activeOpacity: 0.7
17914
+ },
17915
+ isSubmitting ? /* @__PURE__ */ import_react19.default.createElement(import_react_native18.ActivityIndicator, { size: "small", color: "#fff" }) : /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: [
17916
+ styles5.reopenSubmitText,
17917
+ !reopenReason.trim() && styles5.reopenSubmitTextDisabled
17918
+ ] }, "Reopen Issue")
17919
+ ), /* @__PURE__ */ import_react19.default.createElement(
17920
+ import_react_native18.TouchableOpacity,
17921
+ {
17922
+ style: styles5.reopenCancelButton,
17923
+ onPress: () => {
17924
+ setShowReopenForm(false);
17925
+ setReopenReason("");
17926
+ setReopenError(null);
17927
+ },
17928
+ disabled: isSubmitting,
17929
+ activeOpacity: 0.7
17930
+ },
17931
+ /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.reopenCancelText }, "Cancel")
17932
+ ))), issue.screenshotUrls && issue.screenshotUrls.length > 0 && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.screenshotSection }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.screenshotLabel }, "Screenshots (", issue.screenshotUrls.length, ")"), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.screenshotRow }, issue.screenshotUrls.map((url, i) => /* @__PURE__ */ import_react19.default.createElement(import_react_native18.TouchableOpacity, { key: i, onPress: () => import_react_native18.Linking.openURL(url), activeOpacity: 0.7 }, /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Image, { source: { uri: url }, style: styles5.screenshotThumb }))))), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.View, { style: styles5.metaSection }, issue.reporterName && /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.metaText }, "Reported by ", issue.reporterName), /* @__PURE__ */ import_react19.default.createElement(import_react_native18.Text, { style: styles5.metaTextSmall }, "Created ", formatRelativeTime(issue.createdAt), " ", "\xB7", " Updated ", formatRelativeTime(issue.updatedAt))), dashboardUrl && /* @__PURE__ */ import_react19.default.createElement(
17714
17933
  import_react_native18.TouchableOpacity,
17715
17934
  {
17716
17935
  style: styles5.dashboardLink,
@@ -17762,6 +17981,28 @@ function createStyles12() {
17762
17981
  color: colors.textSecondary,
17763
17982
  lineHeight: 19
17764
17983
  },
17984
+ reopenedCard: {
17985
+ backgroundColor: colors.yellowDark,
17986
+ borderWidth: 1,
17987
+ borderColor: colors.yellowBorder,
17988
+ borderRadius: 8,
17989
+ padding: 12,
17990
+ marginBottom: 12
17991
+ },
17992
+ reopenedRow: {
17993
+ flexDirection: "row",
17994
+ alignItems: "center",
17995
+ gap: 8
17996
+ },
17997
+ reopenedIcon: {
17998
+ fontSize: 14
17999
+ },
18000
+ reopenedText: {
18001
+ fontSize: 13,
18002
+ fontWeight: "600",
18003
+ color: colors.yellowLight,
18004
+ flex: 1
18005
+ },
17765
18006
  verifiedCard: {
17766
18007
  backgroundColor: colors.greenDark,
17767
18008
  borderWidth: 1,
@@ -17805,7 +18046,7 @@ function createStyles12() {
17805
18046
  originalBugIcon: {
17806
18047
  fontSize: 16
17807
18048
  },
17808
- originalBugTitle: {
18049
+ originalBugTitleText: {
17809
18050
  fontSize: 13,
17810
18051
  fontWeight: "600",
17811
18052
  color: colors.yellowLight
@@ -17814,6 +18055,89 @@ function createStyles12() {
17814
18055
  fontSize: 12,
17815
18056
  color: colors.yellowSubtle
17816
18057
  },
18058
+ reopenButton: {
18059
+ borderWidth: 1,
18060
+ borderColor: colors.orange,
18061
+ borderRadius: 8,
18062
+ paddingVertical: 10,
18063
+ paddingHorizontal: 16,
18064
+ alignItems: "center",
18065
+ marginBottom: 12
18066
+ },
18067
+ reopenButtonText: {
18068
+ fontSize: 13,
18069
+ fontWeight: "600",
18070
+ color: colors.orange
18071
+ },
18072
+ reopenForm: {
18073
+ backgroundColor: colors.card,
18074
+ borderWidth: 1,
18075
+ borderColor: colors.orange,
18076
+ borderRadius: 8,
18077
+ padding: 12,
18078
+ marginBottom: 12
18079
+ },
18080
+ reopenFormTitle: {
18081
+ fontSize: 13,
18082
+ fontWeight: "600",
18083
+ color: colors.orange,
18084
+ marginBottom: 8
18085
+ },
18086
+ reopenInput: {
18087
+ backgroundColor: colors.bg,
18088
+ borderWidth: 1,
18089
+ borderColor: colors.border,
18090
+ borderRadius: 6,
18091
+ padding: 8,
18092
+ color: colors.textPrimary,
18093
+ fontSize: 13,
18094
+ lineHeight: 18,
18095
+ minHeight: 60,
18096
+ textAlignVertical: "top"
18097
+ },
18098
+ reopenErrorText: {
18099
+ fontSize: 12,
18100
+ color: colors.red,
18101
+ marginTop: 6
18102
+ },
18103
+ reopenActions: {
18104
+ flexDirection: "row",
18105
+ gap: 8,
18106
+ marginTop: 8
18107
+ },
18108
+ reopenSubmitButton: {
18109
+ flex: 1,
18110
+ backgroundColor: colors.orange,
18111
+ borderRadius: 6,
18112
+ paddingVertical: 8,
18113
+ paddingHorizontal: 12,
18114
+ alignItems: "center",
18115
+ justifyContent: "center"
18116
+ },
18117
+ reopenSubmitDisabled: {
18118
+ backgroundColor: colors.card,
18119
+ opacity: 0.7
18120
+ },
18121
+ reopenSubmitText: {
18122
+ fontSize: 13,
18123
+ fontWeight: "600",
18124
+ color: "#fff"
18125
+ },
18126
+ reopenSubmitTextDisabled: {
18127
+ color: colors.textMuted
18128
+ },
18129
+ reopenCancelButton: {
18130
+ borderWidth: 1,
18131
+ borderColor: colors.border,
18132
+ borderRadius: 6,
18133
+ paddingVertical: 8,
18134
+ paddingHorizontal: 12,
18135
+ alignItems: "center"
18136
+ },
18137
+ reopenCancelText: {
18138
+ fontSize: 13,
18139
+ color: colors.textSecondary
18140
+ },
17817
18141
  screenshotSection: {
17818
18142
  marginBottom: 12
17819
18143
  },
package/dist/index.mjs CHANGED
@@ -12197,6 +12197,7 @@ var BugBearClient = class {
12197
12197
  this.navigationHistory = [];
12198
12198
  this.reportSubmitInFlight = false;
12199
12199
  this._queue = null;
12200
+ this._sessionStorage = new LocalStorageAdapter();
12200
12201
  this.realtimeChannels = [];
12201
12202
  this.monitor = null;
12202
12203
  this.initialized = false;
@@ -12274,6 +12275,10 @@ var BugBearClient = class {
12274
12275
  this.initOfflineQueue();
12275
12276
  this.initMonitoring();
12276
12277
  }
12278
+ /** Cache key scoped to the active project. */
12279
+ get sessionCacheKey() {
12280
+ return `bugbear_session_${this.config.projectId ?? "unknown"}`;
12281
+ }
12277
12282
  /** Initialize offline queue if configured. Shared by both init paths. */
12278
12283
  initOfflineQueue() {
12279
12284
  if (this.config.offlineQueue?.enabled) {
@@ -12337,6 +12342,26 @@ var BugBearClient = class {
12337
12342
  await this.pendingInit;
12338
12343
  if (this.initError) throw this.initError;
12339
12344
  }
12345
+ /**
12346
+ * Fire-and-forget call to a dashboard notification endpoint.
12347
+ * Only works when apiKey is configured (needed for API auth).
12348
+ * Failures are silently ignored — notifications are best-effort.
12349
+ */
12350
+ async notifyDashboard(path, body) {
12351
+ if (!this.config.apiKey) return;
12352
+ try {
12353
+ const baseUrl = (this.config.apiBaseUrl || DEFAULT_API_BASE_URL).replace(/\/$/, "");
12354
+ await fetch(`${baseUrl}/api/v1/notifications/${path}`, {
12355
+ method: "POST",
12356
+ headers: {
12357
+ "Content-Type": "application/json",
12358
+ "Authorization": `Bearer ${this.config.apiKey}`
12359
+ },
12360
+ body: JSON.stringify(body)
12361
+ });
12362
+ } catch {
12363
+ }
12364
+ }
12340
12365
  // ── Offline Queue ─────────────────────────────────────────
12341
12366
  /**
12342
12367
  * Access the offline queue (if enabled).
@@ -12363,6 +12388,48 @@ var BugBearClient = class {
12363
12388
  }
12364
12389
  await this._queue.load();
12365
12390
  }
12391
+ // ── Session Cache ──────────────────────────────────────────
12392
+ /**
12393
+ * Swap the session cache storage adapter (for React Native — pass AsyncStorage).
12394
+ * Must be called before getCachedSession() for persistence across app kills.
12395
+ * Web callers don't need this — LocalStorageAdapter is the default.
12396
+ */
12397
+ setSessionStorage(adapter) {
12398
+ this._sessionStorage = adapter;
12399
+ }
12400
+ /**
12401
+ * Cache the active QA session locally for instant restore on app restart.
12402
+ * Pass null to clear the cache (e.g. after ending a session).
12403
+ */
12404
+ async cacheSession(session) {
12405
+ try {
12406
+ if (session) {
12407
+ await this._sessionStorage.setItem(this.sessionCacheKey, JSON.stringify(session));
12408
+ } else {
12409
+ await this._sessionStorage.removeItem(this.sessionCacheKey);
12410
+ }
12411
+ } catch {
12412
+ }
12413
+ }
12414
+ /**
12415
+ * Retrieve the cached QA session. Returns null if no cache, if stale (>24h),
12416
+ * or if parsing fails. The DB fetch in initializeBugBear() is the source of truth.
12417
+ */
12418
+ async getCachedSession() {
12419
+ try {
12420
+ const raw = await this._sessionStorage.getItem(this.sessionCacheKey);
12421
+ if (!raw) return null;
12422
+ const session = JSON.parse(raw);
12423
+ const age = Date.now() - new Date(session.startedAt).getTime();
12424
+ if (age > 24 * 60 * 60 * 1e3) {
12425
+ await this._sessionStorage.removeItem(this.sessionCacheKey);
12426
+ return null;
12427
+ }
12428
+ return session;
12429
+ } catch {
12430
+ return null;
12431
+ }
12432
+ }
12366
12433
  registerQueueHandlers() {
12367
12434
  if (!this._queue) return;
12368
12435
  this._queue.registerHandler("report", async (payload) => {
@@ -12380,6 +12447,11 @@ var BugBearClient = class {
12380
12447
  if (error) return { success: false, error: error.message };
12381
12448
  return { success: true };
12382
12449
  });
12450
+ this._queue.registerHandler("email_capture", async (payload) => {
12451
+ const { error } = await this.supabase.from("email_captures").insert(payload).select("id").single();
12452
+ if (error) return { success: false, error: error.message };
12453
+ return { success: true };
12454
+ });
12383
12455
  }
12384
12456
  // ── Realtime Subscriptions ─────────────────────────────────
12385
12457
  /** Whether realtime is enabled in config. */
@@ -12567,6 +12639,8 @@ var BugBearClient = class {
12567
12639
  if (this.config.onReportSubmitted) {
12568
12640
  this.config.onReportSubmitted(report);
12569
12641
  }
12642
+ this.notifyDashboard("report", { reportId: data.id }).catch(() => {
12643
+ });
12570
12644
  return { success: true, reportId: data.id };
12571
12645
  } catch (err) {
12572
12646
  const message = err instanceof Error ? err.message : "Unknown error";
@@ -12579,6 +12653,44 @@ var BugBearClient = class {
12579
12653
  this.reportSubmitInFlight = false;
12580
12654
  }
12581
12655
  }
12656
+ /**
12657
+ * Capture an email for QA testing.
12658
+ * Called by the email interceptor — not typically called directly.
12659
+ */
12660
+ async captureEmail(payload) {
12661
+ try {
12662
+ await this.ready();
12663
+ if (!payload.subject || !payload.to || payload.to.length === 0) {
12664
+ return { success: false, error: "subject and to are required" };
12665
+ }
12666
+ const record = {
12667
+ project_id: this.config.projectId,
12668
+ to_addresses: payload.to,
12669
+ from_address: payload.from || null,
12670
+ subject: payload.subject,
12671
+ html_content: payload.html || null,
12672
+ text_content: payload.text || null,
12673
+ template_id: payload.templateId || null,
12674
+ metadata: payload.metadata || {},
12675
+ capture_mode: payload.captureMode,
12676
+ was_delivered: payload.wasDelivered,
12677
+ delivery_status: payload.wasDelivered ? "sent" : "pending"
12678
+ };
12679
+ const { data, error } = await this.supabase.from("email_captures").insert(record).select("id").single();
12680
+ if (error) {
12681
+ if (this._queue && isNetworkError(error.message)) {
12682
+ await this._queue.enqueue("email_capture", record);
12683
+ return { success: false, queued: true, error: "Queued \u2014 will send when online" };
12684
+ }
12685
+ console.error("BugBear: Failed to capture email", error.message);
12686
+ return { success: false, error: error.message };
12687
+ }
12688
+ return { success: true, captureId: data.id };
12689
+ } catch (err) {
12690
+ const message = err instanceof Error ? err.message : "Unknown error";
12691
+ return { success: false, error: message };
12692
+ }
12693
+ }
12582
12694
  /**
12583
12695
  * Get assigned tests for current user
12584
12696
  * First looks up the tester by email, then fetches their assignments
@@ -13200,6 +13312,32 @@ var BugBearClient = class {
13200
13312
  return [];
13201
13313
  }
13202
13314
  }
13315
+ /**
13316
+ * Reopen a done issue that the tester believes isn't actually fixed.
13317
+ * Transitions the report from a done status back to 'confirmed'.
13318
+ */
13319
+ async reopenReport(reportId, reason) {
13320
+ try {
13321
+ const testerInfo = await this.getTesterInfo();
13322
+ if (!testerInfo) return { success: false, error: "Not authenticated as tester" };
13323
+ const { data, error } = await this.supabase.rpc("reopen_report", {
13324
+ p_report_id: reportId,
13325
+ p_tester_id: testerInfo.id,
13326
+ p_reason: reason
13327
+ });
13328
+ if (error) {
13329
+ console.error("BugBear: Failed to reopen report", formatPgError(error));
13330
+ return { success: false, error: error.message };
13331
+ }
13332
+ if (!data?.success) {
13333
+ return { success: false, error: data?.error || "Failed to reopen report" };
13334
+ }
13335
+ return { success: true };
13336
+ } catch (err) {
13337
+ console.error("BugBear: Error reopening report", err);
13338
+ return { success: false, error: "Unexpected error" };
13339
+ }
13340
+ }
13203
13341
  /**
13204
13342
  * Basic email format validation (defense in depth)
13205
13343
  */
@@ -13786,7 +13924,7 @@ var BugBearClient = class {
13786
13924
  insertData.attachments = safeAttachments;
13787
13925
  }
13788
13926
  }
13789
- const { error } = await this.supabase.from("discussion_messages").insert(insertData);
13927
+ const { data: msgData, error } = await this.supabase.from("discussion_messages").insert(insertData).select("id").single();
13790
13928
  if (error) {
13791
13929
  if (this._queue && isNetworkError(error.message)) {
13792
13930
  await this._queue.enqueue("message", insertData);
@@ -13795,6 +13933,10 @@ var BugBearClient = class {
13795
13933
  console.error("BugBear: Failed to send message", formatPgError(error));
13796
13934
  return false;
13797
13935
  }
13936
+ if (msgData?.id) {
13937
+ this.notifyDashboard("message", { threadId, messageId: msgData.id }).catch(() => {
13938
+ });
13939
+ }
13798
13940
  await this.markThreadAsRead(threadId);
13799
13941
  return true;
13800
13942
  } catch (err) {
@@ -13920,6 +14062,7 @@ var BugBearClient = class {
13920
14062
  if (!session) {
13921
14063
  return { success: false, error: "Session created but could not be fetched" };
13922
14064
  }
14065
+ await this.cacheSession(session);
13923
14066
  return { success: true, session };
13924
14067
  } catch (err) {
13925
14068
  const message = err instanceof Error ? err.message : "Unknown error";
@@ -13943,6 +14086,7 @@ var BugBearClient = class {
13943
14086
  return { success: false, error: error.message };
13944
14087
  }
13945
14088
  const session = this.transformSession(data);
14089
+ await this.cacheSession(null);
13946
14090
  return { success: true, session };
13947
14091
  } catch (err) {
13948
14092
  const message = err instanceof Error ? err.message : "Unknown error";
@@ -13958,8 +14102,13 @@ var BugBearClient = class {
13958
14102
  const testerInfo = await this.getTesterInfo();
13959
14103
  if (!testerInfo) return null;
13960
14104
  const { data, error } = await this.supabase.from("qa_sessions").select("*").eq("project_id", this.config.projectId).eq("tester_id", testerInfo.id).eq("status", "active").order("started_at", { ascending: false }).limit(1).maybeSingle();
13961
- if (error || !data) return null;
13962
- return this.transformSession(data);
14105
+ if (error || !data) {
14106
+ await this.cacheSession(null);
14107
+ return null;
14108
+ }
14109
+ const session = this.transformSession(data);
14110
+ await this.cacheSession(session);
14111
+ return session;
13963
14112
  } catch (err) {
13964
14113
  console.error("BugBear: Error fetching active session", err);
13965
14114
  return null;
@@ -14477,6 +14626,7 @@ var BugBearContext = createContext({
14477
14626
  issueCounts: { open: 0, done: 0, reopened: 0 },
14478
14627
  refreshIssueCounts: async () => {
14479
14628
  },
14629
+ reopenReport: async () => ({ success: false }),
14480
14630
  queuedCount: 0,
14481
14631
  dashboardUrl: void 0,
14482
14632
  onError: void 0
@@ -14618,6 +14768,14 @@ function BugBearProvider({ config, children, appVersion, enabled = true }) {
14618
14768
  const counts = await client.getIssueCounts();
14619
14769
  setIssueCounts(counts);
14620
14770
  }, [client]);
14771
+ const reopenReport = useCallback(async (reportId, reason) => {
14772
+ if (!client) return { success: false, error: "Client not initialized" };
14773
+ const result = await client.reopenReport(reportId, reason);
14774
+ if (result.success) {
14775
+ await refreshIssueCounts();
14776
+ }
14777
+ return result;
14778
+ }, [client, refreshIssueCounts]);
14621
14779
  const initializeBugBear = useCallback(async (bugBearClient) => {
14622
14780
  setIsLoading(true);
14623
14781
  try {
@@ -14703,6 +14861,10 @@ function BugBearProvider({ config, children, appVersion, enabled = true }) {
14703
14861
  setClient(newClient);
14704
14862
  (async () => {
14705
14863
  try {
14864
+ const cachedSession = await newClient.getCachedSession();
14865
+ if (cachedSession) {
14866
+ setActiveSession(cachedSession);
14867
+ }
14706
14868
  await initializeBugBear(newClient);
14707
14869
  if (newClient.monitor && config.monitoring) {
14708
14870
  const getCurrentRoute = () => contextCapture.getCurrentRoute();
@@ -14837,6 +14999,7 @@ function BugBearProvider({ config, children, appVersion, enabled = true }) {
14837
14999
  // Issue tracking
14838
15000
  issueCounts,
14839
15001
  refreshIssueCounts,
15002
+ reopenReport,
14840
15003
  queuedCount,
14841
15004
  dashboardUrl: config.dashboardUrl,
14842
15005
  onError: config.onError
@@ -14854,7 +15017,7 @@ function BugBearProvider({ config, children, appVersion, enabled = true }) {
14854
15017
  }
14855
15018
 
14856
15019
  // src/BugBearButton.tsx
14857
- import React21, { useState as useState16, useRef as useRef6, useMemo as useMemo16 } from "react";
15020
+ import React21, { useState as useState17, useRef as useRef6, useMemo as useMemo16 } from "react";
14858
15021
  import {
14859
15022
  View as View21,
14860
15023
  Text as Text19,
@@ -14868,7 +15031,7 @@ import {
14868
15031
  Platform as Platform5,
14869
15032
  PanResponder,
14870
15033
  Animated as Animated2,
14871
- ActivityIndicator as ActivityIndicator2,
15034
+ ActivityIndicator as ActivityIndicator3,
14872
15035
  Keyboard as Keyboard4
14873
15036
  } from "react-native";
14874
15037
 
@@ -15583,6 +15746,17 @@ function TestDetailScreen({ testId, nav }) {
15583
15746
  setShowSteps(true);
15584
15747
  setShowDetails(false);
15585
15748
  }, [displayedAssignment?.id]);
15749
+ useEffect4(() => {
15750
+ if (!client || !displayedAssignment || displayedAssignment.status !== "pending") return;
15751
+ let cancelled = false;
15752
+ (async () => {
15753
+ await client.updateAssignmentStatus(displayedAssignment.id, "in_progress");
15754
+ if (!cancelled) await refreshAssignments();
15755
+ })();
15756
+ return () => {
15757
+ cancelled = true;
15758
+ };
15759
+ }, [client, displayedAssignment?.id, displayedAssignment?.status, refreshAssignments]);
15586
15760
  useEffect4(() => {
15587
15761
  const active = displayedAssignment?.status === "in_progress" ? displayedAssignment : null;
15588
15762
  if (!active?.startedAt) {
@@ -15633,17 +15807,6 @@ function TestDetailScreen({ testId, nav }) {
15633
15807
  setIsSubmitting(false);
15634
15808
  }
15635
15809
  }, [client, displayedAssignment, refreshAssignments, nav, isSubmitting]);
15636
- const handleStart = useCallback2(async () => {
15637
- if (!client || !displayedAssignment || isSubmitting) return;
15638
- Keyboard.dismiss();
15639
- setIsSubmitting(true);
15640
- try {
15641
- await client.updateAssignmentStatus(displayedAssignment.id, "in_progress");
15642
- await refreshAssignments();
15643
- } finally {
15644
- setIsSubmitting(false);
15645
- }
15646
- }, [client, displayedAssignment, refreshAssignments, isSubmitting]);
15647
15810
  const handleReopen = useCallback2(async () => {
15648
15811
  if (!client || !displayedAssignment || isSubmitting) return;
15649
15812
  Keyboard.dismiss();
@@ -15789,15 +15952,7 @@ function TestDetailScreen({ testId, nav }) {
15789
15952
  disabled: isSubmitting
15790
15953
  },
15791
15954
  /* @__PURE__ */ React4.createElement(Text2, { style: styles5.passBtnText }, "Change to Pass")
15792
- ))) : displayedAssignment.status === "pending" ? /* @__PURE__ */ React4.createElement(
15793
- TouchableOpacity2,
15794
- {
15795
- style: [styles5.startBtn, isSubmitting && { opacity: 0.5 }],
15796
- onPress: handleStart,
15797
- disabled: isSubmitting
15798
- },
15799
- /* @__PURE__ */ React4.createElement(Text2, { style: styles5.startBtnText }, isSubmitting ? "Starting..." : displayedAssignment.isVerification ? "\u{1F50D} Start Verification" : "\u25B6 Start Test")
15800
- ) : /* @__PURE__ */ React4.createElement(View4, { style: styles5.actionButtons }, /* @__PURE__ */ React4.createElement(TouchableOpacity2, { style: [styles5.actionBtn, styles5.failBtn, isSubmitting && { opacity: 0.5 }], onPress: handleFail, disabled: isSubmitting }, /* @__PURE__ */ React4.createElement(Text2, { style: styles5.failBtnText }, isSubmitting ? displayedAssignment.isVerification ? "Reporting..." : "Failing..." : displayedAssignment.isVerification ? "\u2717 Still Broken" : "Fail")), /* @__PURE__ */ React4.createElement(TouchableOpacity2, { style: [styles5.actionBtn, styles5.skipBtn, isSubmitting && { opacity: 0.5 }], onPress: () => setShowSkipModal(true), disabled: isSubmitting }, /* @__PURE__ */ React4.createElement(Text2, { style: styles5.skipBtnText }, "Skip")), /* @__PURE__ */ React4.createElement(TouchableOpacity2, { style: [styles5.actionBtn, styles5.passBtn, isSubmitting && { opacity: 0.5 }], onPress: handlePass, disabled: isSubmitting }, /* @__PURE__ */ React4.createElement(Text2, { style: styles5.passBtnText }, isSubmitting ? displayedAssignment.isVerification ? "Verifying..." : "Passing..." : displayedAssignment.isVerification ? "\u2713 Fix Verified" : "Pass"))), /* @__PURE__ */ React4.createElement(Modal, { visible: showSkipModal, transparent: true, animationType: "fade" }, /* @__PURE__ */ React4.createElement(View4, { style: styles5.modalOverlay }, /* @__PURE__ */ React4.createElement(View4, { style: styles5.modalContent }, /* @__PURE__ */ React4.createElement(Text2, { style: styles5.modalTitle }, "Skip this test?"), /* @__PURE__ */ React4.createElement(Text2, { style: styles5.modalSubtitle }, "Select a reason:"), [
15955
+ ))) : /* @__PURE__ */ React4.createElement(View4, { style: styles5.actionButtons }, /* @__PURE__ */ React4.createElement(TouchableOpacity2, { style: [styles5.actionBtn, styles5.failBtn, isSubmitting && { opacity: 0.5 }], onPress: handleFail, disabled: isSubmitting }, /* @__PURE__ */ React4.createElement(Text2, { style: styles5.failBtnText }, isSubmitting ? displayedAssignment.isVerification ? "Reporting..." : "Failing..." : displayedAssignment.isVerification ? "\u2717 Still Broken" : "Fail")), /* @__PURE__ */ React4.createElement(TouchableOpacity2, { style: [styles5.actionBtn, styles5.skipBtn, isSubmitting && { opacity: 0.5 }], onPress: () => setShowSkipModal(true), disabled: isSubmitting }, /* @__PURE__ */ React4.createElement(Text2, { style: styles5.skipBtnText }, "Skip")), /* @__PURE__ */ React4.createElement(TouchableOpacity2, { style: [styles5.actionBtn, styles5.passBtn, isSubmitting && { opacity: 0.5 }], onPress: handlePass, disabled: isSubmitting }, /* @__PURE__ */ React4.createElement(Text2, { style: styles5.passBtnText }, isSubmitting ? displayedAssignment.isVerification ? "Verifying..." : "Passing..." : displayedAssignment.isVerification ? "\u2713 Fix Verified" : "Pass"))), /* @__PURE__ */ React4.createElement(Modal, { visible: showSkipModal, transparent: true, animationType: "fade" }, /* @__PURE__ */ React4.createElement(View4, { style: styles5.modalOverlay }, /* @__PURE__ */ React4.createElement(View4, { style: styles5.modalContent }, /* @__PURE__ */ React4.createElement(Text2, { style: styles5.modalTitle }, "Skip this test?"), /* @__PURE__ */ React4.createElement(Text2, { style: styles5.modalSubtitle }, "Select a reason:"), [
15801
15956
  { reason: "blocked", label: "\u{1F6AB} Blocked by a bug" },
15802
15957
  { reason: "not_ready", label: "\u{1F6A7} Feature not ready" },
15803
15958
  { reason: "dependency", label: "\u{1F517} Needs another test first" },
@@ -15911,9 +16066,6 @@ function createStyles2() {
15911
16066
  completedLabel: { fontSize: 13, fontWeight: "600", color: colors.textSecondary },
15912
16067
  reopenBtn: { backgroundColor: colors.card, borderWidth: 1, borderColor: colors.blue },
15913
16068
  reopenBtnText: { fontSize: 14, fontWeight: "600", color: colors.blue },
15914
- // Start Test button
15915
- startBtn: { paddingVertical: 16, borderRadius: 12, alignItems: "center", backgroundColor: colors.blueSurface, borderWidth: 1, borderColor: colors.blueAccent, marginTop: 8 },
15916
- startBtnText: { fontSize: 16, fontWeight: "600", color: colors.blueText },
15917
16069
  // Action buttons
15918
16070
  actionButtons: { flexDirection: "row", gap: 10, marginTop: 8 },
15919
16071
  actionBtn: { flex: 1, paddingVertical: 14, borderRadius: 12, alignItems: "center" },
@@ -17665,11 +17817,17 @@ function createStyles11() {
17665
17817
  }
17666
17818
 
17667
17819
  // src/widget/screens/IssueDetailScreen.tsx
17668
- import React17, { useMemo as useMemo12 } from "react";
17669
- import { View as View17, Text as Text15, Image as Image3, StyleSheet as StyleSheet17, Linking as Linking4, TouchableOpacity as TouchableOpacity14 } from "react-native";
17820
+ import React17, { useMemo as useMemo12, useState as useState13, useCallback as useCallback5 } from "react";
17821
+ import { View as View17, Text as Text15, Image as Image3, StyleSheet as StyleSheet17, Linking as Linking4, TouchableOpacity as TouchableOpacity14, TextInput as TextInput8, ActivityIndicator as ActivityIndicator2 } from "react-native";
17822
+ var DONE_STATUSES = ["verified", "resolved", "closed", "reviewed"];
17670
17823
  function IssueDetailScreen({ nav, issue }) {
17671
- const { dashboardUrl, widgetColorScheme } = useBugBear();
17824
+ const { dashboardUrl, widgetColorScheme, reopenReport } = useBugBear();
17672
17825
  const styles5 = useMemo12(() => createStyles12(), [widgetColorScheme]);
17826
+ const [showReopenForm, setShowReopenForm] = useState13(false);
17827
+ const [reopenReason, setReopenReason] = useState13("");
17828
+ const [isSubmitting, setIsSubmitting] = useState13(false);
17829
+ const [reopenError, setReopenError] = useState13(null);
17830
+ const [wasReopened, setWasReopened] = useState13(false);
17673
17831
  const STATUS_LABELS = useMemo12(() => ({
17674
17832
  new: { label: "New", bg: colors.blueDark, color: colors.blueLight },
17675
17833
  triaging: { label: "Triaging", bg: colors.blueDark, color: colors.blueLight },
@@ -17692,7 +17850,68 @@ function IssueDetailScreen({ nav, issue }) {
17692
17850
  }), [widgetColorScheme]);
17693
17851
  const statusConfig = STATUS_LABELS[issue.status] || { label: issue.status, bg: colors.card, color: colors.textSecondary };
17694
17852
  const severityConfig = issue.severity ? SEVERITY_CONFIG[issue.severity] : null;
17695
- return /* @__PURE__ */ React17.createElement(View17, null, /* @__PURE__ */ React17.createElement(View17, { style: styles5.badgeRow }, /* @__PURE__ */ React17.createElement(View17, { style: [styles5.badge, { backgroundColor: statusConfig.bg }] }, /* @__PURE__ */ React17.createElement(Text15, { style: [styles5.badgeText, { color: statusConfig.color }] }, statusConfig.label)), severityConfig && /* @__PURE__ */ React17.createElement(View17, { style: [styles5.badge, { backgroundColor: severityConfig.bg }] }, /* @__PURE__ */ React17.createElement(Text15, { style: [styles5.badgeText, { color: severityConfig.color }] }, severityConfig.label))), /* @__PURE__ */ React17.createElement(Text15, { style: styles5.title }, issue.title), issue.route && /* @__PURE__ */ React17.createElement(Text15, { style: styles5.route }, issue.route), issue.description && /* @__PURE__ */ React17.createElement(View17, { style: styles5.descriptionCard }, /* @__PURE__ */ React17.createElement(Text15, { style: styles5.descriptionText }, issue.description)), issue.verifiedByName && /* @__PURE__ */ React17.createElement(View17, { style: styles5.verifiedCard }, /* @__PURE__ */ React17.createElement(View17, { style: styles5.verifiedHeader }, /* @__PURE__ */ React17.createElement(Text15, { style: styles5.verifiedIcon }, "\u2705"), /* @__PURE__ */ React17.createElement(Text15, { style: styles5.verifiedTitle }, "Retesting Proof")), /* @__PURE__ */ React17.createElement(Text15, { style: styles5.verifiedBody }, "Verified by ", issue.verifiedByName, issue.verifiedAt && ` on ${new Date(issue.verifiedAt).toLocaleDateString(void 0, { month: "short", day: "numeric", year: "numeric" })}`)), issue.originalBugTitle && /* @__PURE__ */ React17.createElement(View17, { style: styles5.originalBugCard }, /* @__PURE__ */ React17.createElement(View17, { style: styles5.originalBugHeader }, /* @__PURE__ */ React17.createElement(Text15, { style: styles5.originalBugIcon }, "\u{1F504}"), /* @__PURE__ */ React17.createElement(Text15, { style: styles5.originalBugTitle }, "Original Bug")), /* @__PURE__ */ React17.createElement(Text15, { style: styles5.originalBugBody }, "Retest of: ", issue.originalBugTitle)), issue.screenshotUrls && issue.screenshotUrls.length > 0 && /* @__PURE__ */ React17.createElement(View17, { style: styles5.screenshotSection }, /* @__PURE__ */ React17.createElement(Text15, { style: styles5.screenshotLabel }, "Screenshots (", issue.screenshotUrls.length, ")"), /* @__PURE__ */ React17.createElement(View17, { style: styles5.screenshotRow }, issue.screenshotUrls.map((url, i) => /* @__PURE__ */ React17.createElement(TouchableOpacity14, { key: i, onPress: () => Linking4.openURL(url), activeOpacity: 0.7 }, /* @__PURE__ */ React17.createElement(Image3, { source: { uri: url }, style: styles5.screenshotThumb }))))), /* @__PURE__ */ React17.createElement(View17, { style: styles5.metaSection }, issue.reporterName && /* @__PURE__ */ React17.createElement(Text15, { style: styles5.metaText }, "Reported by ", issue.reporterName), /* @__PURE__ */ React17.createElement(Text15, { style: styles5.metaTextSmall }, "Created ", formatRelativeTime(issue.createdAt), " ", "\xB7", " Updated ", formatRelativeTime(issue.updatedAt))), dashboardUrl && /* @__PURE__ */ React17.createElement(
17853
+ const isDone = DONE_STATUSES.includes(issue.status);
17854
+ const handleReopen = useCallback5(async () => {
17855
+ if (!reopenReason.trim()) return;
17856
+ setIsSubmitting(true);
17857
+ setReopenError(null);
17858
+ const result = await reopenReport(issue.id, reopenReason.trim());
17859
+ setIsSubmitting(false);
17860
+ if (result.success) {
17861
+ setWasReopened(true);
17862
+ setShowReopenForm(false);
17863
+ } else {
17864
+ setReopenError(result.error || "Failed to reopen");
17865
+ }
17866
+ }, [reopenReason, reopenReport, issue.id]);
17867
+ return /* @__PURE__ */ React17.createElement(View17, null, /* @__PURE__ */ React17.createElement(View17, { style: styles5.badgeRow }, /* @__PURE__ */ React17.createElement(View17, { style: [styles5.badge, { backgroundColor: wasReopened ? colors.yellowDark : statusConfig.bg }] }, /* @__PURE__ */ React17.createElement(Text15, { style: [styles5.badgeText, { color: wasReopened ? colors.yellowLight : statusConfig.color }] }, wasReopened ? "Reopened" : statusConfig.label)), severityConfig && /* @__PURE__ */ React17.createElement(View17, { style: [styles5.badge, { backgroundColor: severityConfig.bg }] }, /* @__PURE__ */ React17.createElement(Text15, { style: [styles5.badgeText, { color: severityConfig.color }] }, severityConfig.label))), /* @__PURE__ */ React17.createElement(Text15, { style: styles5.title }, issue.title), issue.route && /* @__PURE__ */ React17.createElement(Text15, { style: styles5.route }, issue.route), issue.description && /* @__PURE__ */ React17.createElement(View17, { style: styles5.descriptionCard }, /* @__PURE__ */ React17.createElement(Text15, { style: styles5.descriptionText }, issue.description)), wasReopened && /* @__PURE__ */ React17.createElement(View17, { style: styles5.reopenedCard }, /* @__PURE__ */ React17.createElement(View17, { style: styles5.reopenedRow }, /* @__PURE__ */ React17.createElement(Text15, { style: styles5.reopenedIcon }, "\u{1F504}"), /* @__PURE__ */ React17.createElement(Text15, { style: styles5.reopenedText }, "Issue reopened \u2014 your team has been notified"))), issue.verifiedByName && !wasReopened && /* @__PURE__ */ React17.createElement(View17, { style: styles5.verifiedCard }, /* @__PURE__ */ React17.createElement(View17, { style: styles5.verifiedHeader }, /* @__PURE__ */ React17.createElement(Text15, { style: styles5.verifiedIcon }, "\u2705"), /* @__PURE__ */ React17.createElement(Text15, { style: styles5.verifiedTitle }, "Retesting Proof")), /* @__PURE__ */ React17.createElement(Text15, { style: styles5.verifiedBody }, "Verified by ", issue.verifiedByName, issue.verifiedAt && ` on ${new Date(issue.verifiedAt).toLocaleDateString(void 0, { month: "short", day: "numeric", year: "numeric" })}`)), issue.originalBugTitle && /* @__PURE__ */ React17.createElement(View17, { style: styles5.originalBugCard }, /* @__PURE__ */ React17.createElement(View17, { style: styles5.originalBugHeader }, /* @__PURE__ */ React17.createElement(Text15, { style: styles5.originalBugIcon }, "\u{1F504}"), /* @__PURE__ */ React17.createElement(Text15, { style: styles5.originalBugTitleText }, "Original Bug")), /* @__PURE__ */ React17.createElement(Text15, { style: styles5.originalBugBody }, "Retest of: ", issue.originalBugTitle)), isDone && !wasReopened && !showReopenForm && /* @__PURE__ */ React17.createElement(
17868
+ TouchableOpacity14,
17869
+ {
17870
+ style: styles5.reopenButton,
17871
+ onPress: () => setShowReopenForm(true),
17872
+ activeOpacity: 0.7
17873
+ },
17874
+ /* @__PURE__ */ React17.createElement(Text15, { style: styles5.reopenButtonText }, "\u{1F504}", " Not Fixed \u2014 Reopen Issue")
17875
+ ), showReopenForm && !wasReopened && /* @__PURE__ */ React17.createElement(View17, { style: styles5.reopenForm }, /* @__PURE__ */ React17.createElement(Text15, { style: styles5.reopenFormTitle }, "Why isn't this fixed?"), /* @__PURE__ */ React17.createElement(
17876
+ TextInput8,
17877
+ {
17878
+ value: reopenReason,
17879
+ onChangeText: setReopenReason,
17880
+ placeholder: "Describe what you're still seeing...",
17881
+ placeholderTextColor: colors.textMuted,
17882
+ style: styles5.reopenInput,
17883
+ multiline: true,
17884
+ autoFocus: true
17885
+ }
17886
+ ), reopenError && /* @__PURE__ */ React17.createElement(Text15, { style: styles5.reopenErrorText }, reopenError), /* @__PURE__ */ React17.createElement(View17, { style: styles5.reopenActions }, /* @__PURE__ */ React17.createElement(
17887
+ TouchableOpacity14,
17888
+ {
17889
+ style: [
17890
+ styles5.reopenSubmitButton,
17891
+ (!reopenReason.trim() || isSubmitting) && styles5.reopenSubmitDisabled
17892
+ ],
17893
+ onPress: handleReopen,
17894
+ disabled: isSubmitting || !reopenReason.trim(),
17895
+ activeOpacity: 0.7
17896
+ },
17897
+ isSubmitting ? /* @__PURE__ */ React17.createElement(ActivityIndicator2, { size: "small", color: "#fff" }) : /* @__PURE__ */ React17.createElement(Text15, { style: [
17898
+ styles5.reopenSubmitText,
17899
+ !reopenReason.trim() && styles5.reopenSubmitTextDisabled
17900
+ ] }, "Reopen Issue")
17901
+ ), /* @__PURE__ */ React17.createElement(
17902
+ TouchableOpacity14,
17903
+ {
17904
+ style: styles5.reopenCancelButton,
17905
+ onPress: () => {
17906
+ setShowReopenForm(false);
17907
+ setReopenReason("");
17908
+ setReopenError(null);
17909
+ },
17910
+ disabled: isSubmitting,
17911
+ activeOpacity: 0.7
17912
+ },
17913
+ /* @__PURE__ */ React17.createElement(Text15, { style: styles5.reopenCancelText }, "Cancel")
17914
+ ))), issue.screenshotUrls && issue.screenshotUrls.length > 0 && /* @__PURE__ */ React17.createElement(View17, { style: styles5.screenshotSection }, /* @__PURE__ */ React17.createElement(Text15, { style: styles5.screenshotLabel }, "Screenshots (", issue.screenshotUrls.length, ")"), /* @__PURE__ */ React17.createElement(View17, { style: styles5.screenshotRow }, issue.screenshotUrls.map((url, i) => /* @__PURE__ */ React17.createElement(TouchableOpacity14, { key: i, onPress: () => Linking4.openURL(url), activeOpacity: 0.7 }, /* @__PURE__ */ React17.createElement(Image3, { source: { uri: url }, style: styles5.screenshotThumb }))))), /* @__PURE__ */ React17.createElement(View17, { style: styles5.metaSection }, issue.reporterName && /* @__PURE__ */ React17.createElement(Text15, { style: styles5.metaText }, "Reported by ", issue.reporterName), /* @__PURE__ */ React17.createElement(Text15, { style: styles5.metaTextSmall }, "Created ", formatRelativeTime(issue.createdAt), " ", "\xB7", " Updated ", formatRelativeTime(issue.updatedAt))), dashboardUrl && /* @__PURE__ */ React17.createElement(
17696
17915
  TouchableOpacity14,
17697
17916
  {
17698
17917
  style: styles5.dashboardLink,
@@ -17744,6 +17963,28 @@ function createStyles12() {
17744
17963
  color: colors.textSecondary,
17745
17964
  lineHeight: 19
17746
17965
  },
17966
+ reopenedCard: {
17967
+ backgroundColor: colors.yellowDark,
17968
+ borderWidth: 1,
17969
+ borderColor: colors.yellowBorder,
17970
+ borderRadius: 8,
17971
+ padding: 12,
17972
+ marginBottom: 12
17973
+ },
17974
+ reopenedRow: {
17975
+ flexDirection: "row",
17976
+ alignItems: "center",
17977
+ gap: 8
17978
+ },
17979
+ reopenedIcon: {
17980
+ fontSize: 14
17981
+ },
17982
+ reopenedText: {
17983
+ fontSize: 13,
17984
+ fontWeight: "600",
17985
+ color: colors.yellowLight,
17986
+ flex: 1
17987
+ },
17747
17988
  verifiedCard: {
17748
17989
  backgroundColor: colors.greenDark,
17749
17990
  borderWidth: 1,
@@ -17787,7 +18028,7 @@ function createStyles12() {
17787
18028
  originalBugIcon: {
17788
18029
  fontSize: 16
17789
18030
  },
17790
- originalBugTitle: {
18031
+ originalBugTitleText: {
17791
18032
  fontSize: 13,
17792
18033
  fontWeight: "600",
17793
18034
  color: colors.yellowLight
@@ -17796,6 +18037,89 @@ function createStyles12() {
17796
18037
  fontSize: 12,
17797
18038
  color: colors.yellowSubtle
17798
18039
  },
18040
+ reopenButton: {
18041
+ borderWidth: 1,
18042
+ borderColor: colors.orange,
18043
+ borderRadius: 8,
18044
+ paddingVertical: 10,
18045
+ paddingHorizontal: 16,
18046
+ alignItems: "center",
18047
+ marginBottom: 12
18048
+ },
18049
+ reopenButtonText: {
18050
+ fontSize: 13,
18051
+ fontWeight: "600",
18052
+ color: colors.orange
18053
+ },
18054
+ reopenForm: {
18055
+ backgroundColor: colors.card,
18056
+ borderWidth: 1,
18057
+ borderColor: colors.orange,
18058
+ borderRadius: 8,
18059
+ padding: 12,
18060
+ marginBottom: 12
18061
+ },
18062
+ reopenFormTitle: {
18063
+ fontSize: 13,
18064
+ fontWeight: "600",
18065
+ color: colors.orange,
18066
+ marginBottom: 8
18067
+ },
18068
+ reopenInput: {
18069
+ backgroundColor: colors.bg,
18070
+ borderWidth: 1,
18071
+ borderColor: colors.border,
18072
+ borderRadius: 6,
18073
+ padding: 8,
18074
+ color: colors.textPrimary,
18075
+ fontSize: 13,
18076
+ lineHeight: 18,
18077
+ minHeight: 60,
18078
+ textAlignVertical: "top"
18079
+ },
18080
+ reopenErrorText: {
18081
+ fontSize: 12,
18082
+ color: colors.red,
18083
+ marginTop: 6
18084
+ },
18085
+ reopenActions: {
18086
+ flexDirection: "row",
18087
+ gap: 8,
18088
+ marginTop: 8
18089
+ },
18090
+ reopenSubmitButton: {
18091
+ flex: 1,
18092
+ backgroundColor: colors.orange,
18093
+ borderRadius: 6,
18094
+ paddingVertical: 8,
18095
+ paddingHorizontal: 12,
18096
+ alignItems: "center",
18097
+ justifyContent: "center"
18098
+ },
18099
+ reopenSubmitDisabled: {
18100
+ backgroundColor: colors.card,
18101
+ opacity: 0.7
18102
+ },
18103
+ reopenSubmitText: {
18104
+ fontSize: 13,
18105
+ fontWeight: "600",
18106
+ color: "#fff"
18107
+ },
18108
+ reopenSubmitTextDisabled: {
18109
+ color: colors.textMuted
18110
+ },
18111
+ reopenCancelButton: {
18112
+ borderWidth: 1,
18113
+ borderColor: colors.border,
18114
+ borderRadius: 6,
18115
+ paddingVertical: 8,
18116
+ paddingHorizontal: 12,
18117
+ alignItems: "center"
18118
+ },
18119
+ reopenCancelText: {
18120
+ fontSize: 13,
18121
+ color: colors.textSecondary
18122
+ },
17799
18123
  screenshotSection: {
17800
18124
  marginBottom: 12
17801
18125
  },
@@ -17847,13 +18171,13 @@ function createStyles12() {
17847
18171
  }
17848
18172
 
17849
18173
  // src/widget/screens/SessionStartScreen.tsx
17850
- import React18, { useState as useState13, useMemo as useMemo13 } from "react";
17851
- import { View as View18, Text as Text16, TextInput as TextInput8, TouchableOpacity as TouchableOpacity15, StyleSheet as StyleSheet18, Keyboard as Keyboard2 } from "react-native";
18174
+ import React18, { useState as useState14, useMemo as useMemo13 } from "react";
18175
+ import { View as View18, Text as Text16, TextInput as TextInput9, TouchableOpacity as TouchableOpacity15, StyleSheet as StyleSheet18, Keyboard as Keyboard2 } from "react-native";
17852
18176
  function SessionStartScreen({ nav }) {
17853
18177
  const { startSession, assignments, widgetColorScheme } = useBugBear();
17854
18178
  const styles5 = useMemo13(() => createStyles13(), [widgetColorScheme]);
17855
- const [focusArea, setFocusArea] = useState13("");
17856
- const [isStarting, setIsStarting] = useState13(false);
18179
+ const [focusArea, setFocusArea] = useState14("");
18180
+ const [isStarting, setIsStarting] = useState14(false);
17857
18181
  const trackNames = Array.from(new Set(
17858
18182
  assignments.filter((a) => a.testCase.track?.name).map((a) => a.testCase.track.name)
17859
18183
  )).slice(0, 6);
@@ -17873,7 +18197,7 @@ function SessionStartScreen({ nav }) {
17873
18197
  }
17874
18198
  };
17875
18199
  return /* @__PURE__ */ React18.createElement(View18, null, /* @__PURE__ */ React18.createElement(View18, { style: styles5.header }, /* @__PURE__ */ React18.createElement(Text16, { style: styles5.headerIcon }, "\u{1F50D}"), /* @__PURE__ */ React18.createElement(Text16, { style: styles5.headerDesc }, "Start an exploratory QA session. Log findings as you go \u2014 bugs, concerns, suggestions, or questions.")), /* @__PURE__ */ React18.createElement(View18, { style: styles5.inputSection }, /* @__PURE__ */ React18.createElement(Text16, { style: styles5.label }, "What are you testing?"), /* @__PURE__ */ React18.createElement(
17876
- TextInput8,
18200
+ TextInput9,
17877
18201
  {
17878
18202
  value: focusArea,
17879
18203
  onChangeText: setFocusArea,
@@ -17991,7 +18315,7 @@ function createStyles13() {
17991
18315
  }
17992
18316
 
17993
18317
  // src/widget/screens/SessionActiveScreen.tsx
17994
- import React19, { useState as useState14, useEffect as useEffect11, useRef as useRef5, useMemo as useMemo14 } from "react";
18318
+ import React19, { useState as useState15, useEffect as useEffect11, useRef as useRef5, useMemo as useMemo14 } from "react";
17995
18319
  import { View as View19, Text as Text17, TouchableOpacity as TouchableOpacity16, StyleSheet as StyleSheet19 } from "react-native";
17996
18320
  function SessionActiveScreen({ nav }) {
17997
18321
  const { activeSession, sessionFindings, endSession, refreshSession, widgetColorScheme } = useBugBear();
@@ -18002,8 +18326,8 @@ function SessionActiveScreen({ nav }) {
18002
18326
  suggestion: { icon: "\u{1F4A1}", label: "Suggestion", color: colors.blue },
18003
18327
  question: { icon: "\u2753", label: "Question", color: colors.violet }
18004
18328
  }), [widgetColorScheme]);
18005
- const [isEnding, setIsEnding] = useState14(false);
18006
- const [elapsed, setElapsed] = useState14(0);
18329
+ const [isEnding, setIsEnding] = useState15(false);
18330
+ const [elapsed, setElapsed] = useState15(0);
18007
18331
  const timerRef = useRef5(null);
18008
18332
  useEffect11(() => {
18009
18333
  refreshSession();
@@ -18228,8 +18552,8 @@ function createStyles14() {
18228
18552
  }
18229
18553
 
18230
18554
  // src/widget/screens/SessionFindingScreen.tsx
18231
- import React20, { useState as useState15, useMemo as useMemo15 } from "react";
18232
- import { View as View20, Text as Text18, TextInput as TextInput9, TouchableOpacity as TouchableOpacity17, StyleSheet as StyleSheet20, Keyboard as Keyboard3 } from "react-native";
18555
+ import React20, { useState as useState16, useMemo as useMemo15 } from "react";
18556
+ import { View as View20, Text as Text18, TextInput as TextInput10, TouchableOpacity as TouchableOpacity17, StyleSheet as StyleSheet20, Keyboard as Keyboard3 } from "react-native";
18233
18557
  var FINDING_TYPES = [
18234
18558
  { value: "bug", icon: "\u{1F41B}", label: "Bug" },
18235
18559
  { value: "concern", icon: "\u26A0\uFE0F", label: "Concern" },
@@ -18246,11 +18570,11 @@ function SessionFindingScreen({ nav }) {
18246
18570
  { value: "low", label: "Low", color: colors.textMuted },
18247
18571
  { value: "observation", label: "Note", color: colors.textDim }
18248
18572
  ], [widgetColorScheme]);
18249
- const [type, setType] = useState15("bug");
18250
- const [severity, setSeverity] = useState15("medium");
18251
- const [title, setTitle] = useState15("");
18252
- const [description, setDescription] = useState15("");
18253
- const [isSubmitting, setIsSubmitting] = useState15(false);
18573
+ const [type, setType] = useState16("bug");
18574
+ const [severity, setSeverity] = useState16("medium");
18575
+ const [title, setTitle] = useState16("");
18576
+ const [description, setDescription] = useState16("");
18577
+ const [isSubmitting, setIsSubmitting] = useState16(false);
18254
18578
  const handleSubmit = async () => {
18255
18579
  if (!title.trim() || isSubmitting) return;
18256
18580
  Keyboard3.dismiss();
@@ -18293,7 +18617,7 @@ function SessionFindingScreen({ nav }) {
18293
18617
  },
18294
18618
  /* @__PURE__ */ React20.createElement(Text18, { style: [styles5.severityText, severity === s2.value && { color: s2.color }] }, s2.label)
18295
18619
  )))), /* @__PURE__ */ React20.createElement(View20, { style: styles5.inputSection }, /* @__PURE__ */ React20.createElement(
18296
- TextInput9,
18620
+ TextInput10,
18297
18621
  {
18298
18622
  value: title,
18299
18623
  onChangeText: setTitle,
@@ -18303,7 +18627,7 @@ function SessionFindingScreen({ nav }) {
18303
18627
  returnKeyType: "next"
18304
18628
  }
18305
18629
  )), /* @__PURE__ */ React20.createElement(View20, { style: styles5.inputSection }, /* @__PURE__ */ React20.createElement(
18306
- TextInput9,
18630
+ TextInput10,
18307
18631
  {
18308
18632
  value: description,
18309
18633
  onChangeText: setDescription,
@@ -18448,7 +18772,7 @@ function BugBearButton({
18448
18772
  }) {
18449
18773
  const { shouldShowWidget, testerInfo, isLoading, unreadCount, assignments, widgetMode, widgetColorScheme } = useBugBear();
18450
18774
  const { currentScreen, canGoBack, push, pop, replace, reset } = useNavigation();
18451
- const [modalVisible, setModalVisible] = useState16(false);
18775
+ const [modalVisible, setModalVisible] = useState17(false);
18452
18776
  const styles5 = useMemo16(() => createStyles16(), [widgetColorScheme]);
18453
18777
  const screenCaptureRef = useRef6(null);
18454
18778
  const openModal = () => {
@@ -18663,7 +18987,7 @@ function BugBearButton({
18663
18987
  keyboardShouldPersistTaps: "handled",
18664
18988
  showsVerticalScrollIndicator: false
18665
18989
  },
18666
- isLoading ? /* @__PURE__ */ React21.createElement(View21, { style: styles5.loadingContainer }, /* @__PURE__ */ React21.createElement(ActivityIndicator2, { size: "large", color: colors.blue }), /* @__PURE__ */ React21.createElement(Text19, { style: styles5.loadingText }, "Loading...")) : renderScreen()
18990
+ isLoading ? /* @__PURE__ */ React21.createElement(View21, { style: styles5.loadingContainer }, /* @__PURE__ */ React21.createElement(ActivityIndicator3, { size: "large", color: colors.blue }), /* @__PURE__ */ React21.createElement(Text19, { style: styles5.loadingText }, "Loading...")) : renderScreen()
18667
18991
  ))
18668
18992
  )
18669
18993
  ));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bbearai/react-native",
3
- "version": "0.8.2",
3
+ "version": "0.8.3",
4
4
  "description": "BugBear React Native components for mobile apps",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",