@bbearai/react-native 0.6.3 → 0.7.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.
Files changed (3) hide show
  1. package/dist/index.js +302 -43
  2. package/dist/index.mjs +303 -44
  3. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -11540,6 +11540,7 @@ var ContextCaptureManager = class {
11540
11540
  this.networkRequests = [];
11541
11541
  this.navigationHistory = [];
11542
11542
  this.originalConsole = {};
11543
+ this.fetchHost = null;
11543
11544
  this.isCapturing = false;
11544
11545
  }
11545
11546
  /**
@@ -11562,8 +11563,9 @@ var ContextCaptureManager = class {
11562
11563
  if (this.originalConsole.warn) console.warn = this.originalConsole.warn;
11563
11564
  if (this.originalConsole.error) console.error = this.originalConsole.error;
11564
11565
  if (this.originalConsole.info) console.info = this.originalConsole.info;
11565
- if (this.originalFetch && typeof window !== "undefined") {
11566
- window.fetch = this.originalFetch;
11566
+ if (this.originalFetch && this.fetchHost) {
11567
+ this.fetchHost.fetch = this.originalFetch;
11568
+ this.fetchHost = null;
11567
11569
  }
11568
11570
  if (typeof window !== "undefined" && typeof history !== "undefined") {
11569
11571
  if (this.originalPushState) {
@@ -11672,15 +11674,19 @@ var ContextCaptureManager = class {
11672
11674
  });
11673
11675
  }
11674
11676
  captureFetch() {
11675
- if (typeof window === "undefined" || typeof fetch === "undefined" || typeof document === "undefined") return;
11676
- this.originalFetch = window.fetch;
11677
+ if (typeof fetch === "undefined") return;
11678
+ const host = typeof window !== "undefined" && typeof window.fetch === "function" ? window : typeof globalThis !== "undefined" && typeof globalThis.fetch === "function" ? globalThis : null;
11679
+ if (!host) return;
11680
+ const canCloneResponse = typeof document !== "undefined";
11681
+ this.fetchHost = host;
11682
+ this.originalFetch = host.fetch;
11677
11683
  const self2 = this;
11678
- window.fetch = async function(input, init) {
11684
+ host.fetch = async function(input, init) {
11679
11685
  const startTime = Date.now();
11680
11686
  const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
11681
11687
  const method = init?.method || "GET";
11682
11688
  try {
11683
- const response = await self2.originalFetch.call(window, input, init);
11689
+ const response = await self2.originalFetch.call(host, input, init);
11684
11690
  const requestEntry = {
11685
11691
  method,
11686
11692
  url: url.slice(0, 200),
@@ -11689,7 +11695,7 @@ var ContextCaptureManager = class {
11689
11695
  duration: Date.now() - startTime,
11690
11696
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
11691
11697
  };
11692
- if (response.status >= 400) {
11698
+ if (canCloneResponse && response.status >= 400) {
11693
11699
  try {
11694
11700
  const cloned = response.clone();
11695
11701
  const body = await cloned.text();
@@ -11933,29 +11939,140 @@ var formatPgError = (e) => {
11933
11939
  const { message, code, details, hint } = e;
11934
11940
  return { message, code, details, hint };
11935
11941
  };
11942
+ var DEFAULT_API_BASE_URL = "https://app.bugbear.ai";
11943
+ var CONFIG_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
11944
+ var CONFIG_CACHE_PREFIX = "bugbear_config_";
11936
11945
  var BugBearClient = class {
11937
11946
  constructor(config) {
11938
11947
  this.navigationHistory = [];
11939
11948
  this.reportSubmitInFlight = false;
11940
11949
  this._queue = null;
11941
11950
  this.realtimeChannels = [];
11942
- if (!config.supabaseUrl) {
11943
- throw new Error("BugBear: supabaseUrl is required. Get it from your BugBear project settings.");
11944
- }
11945
- if (!config.supabaseAnonKey) {
11946
- throw new Error("BugBear: supabaseAnonKey is required. Get it from your BugBear project settings.");
11947
- }
11951
+ this.initialized = false;
11952
+ this.initError = null;
11948
11953
  this.config = config;
11949
- this.supabase = createClient(config.supabaseUrl, config.supabaseAnonKey);
11950
- if (config.offlineQueue?.enabled) {
11954
+ if (config.apiKey) {
11955
+ this.pendingInit = this.resolveFromApiKey(config.apiKey);
11956
+ } else if (config.supabaseUrl && config.supabaseAnonKey) {
11957
+ if (!config.projectId) {
11958
+ throw new Error(
11959
+ "BugBear: projectId is required when using explicit Supabase credentials. Tip: Use apiKey instead for simpler setup \u2014 it resolves everything automatically."
11960
+ );
11961
+ }
11962
+ this.supabase = createClient(config.supabaseUrl, config.supabaseAnonKey);
11963
+ this.initialized = true;
11964
+ this.pendingInit = Promise.resolve();
11965
+ this.initOfflineQueue();
11966
+ } else {
11967
+ throw new Error(
11968
+ "BugBear: Missing configuration. Provide either:\n \u2022 apiKey (recommended) \u2014 resolves everything automatically\n \u2022 projectId + supabaseUrl + supabaseAnonKey \u2014 explicit credentials\n\nGet your API key at https://app.bugbear.ai/settings/projects"
11969
+ );
11970
+ }
11971
+ }
11972
+ /** Whether the client is ready for requests. */
11973
+ get isReady() {
11974
+ return this.initialized;
11975
+ }
11976
+ /** Wait until the client is ready. Throws if initialization failed. */
11977
+ async ready() {
11978
+ await this.pendingInit;
11979
+ if (this.initError) throw this.initError;
11980
+ }
11981
+ /**
11982
+ * Resolve Supabase credentials from a BugBear API key.
11983
+ * Checks localStorage cache first, falls back to /api/v1/config.
11984
+ */
11985
+ async resolveFromApiKey(apiKey) {
11986
+ try {
11987
+ const cached = this.readConfigCache(apiKey);
11988
+ if (cached) {
11989
+ this.applyResolvedConfig(cached);
11990
+ return;
11991
+ }
11992
+ const baseUrl = (this.config.apiBaseUrl || DEFAULT_API_BASE_URL).replace(/\/$/, "");
11993
+ const response = await fetch(`${baseUrl}/api/v1/config`, {
11994
+ headers: { Authorization: `Bearer ${apiKey}` }
11995
+ });
11996
+ if (!response.ok) {
11997
+ const body = await response.json().catch(() => ({}));
11998
+ const message = body.error || `HTTP ${response.status}`;
11999
+ throw new Error(
12000
+ `BugBear: Invalid API key \u2014 ${message}. Get yours at https://app.bugbear.ai/settings/projects`
12001
+ );
12002
+ }
12003
+ const data = await response.json();
12004
+ this.writeConfigCache(apiKey, data);
12005
+ this.applyResolvedConfig(data);
12006
+ } catch (err) {
12007
+ this.initError = err instanceof Error ? err : new Error(String(err));
12008
+ this.config.onError?.(this.initError, { context: "apikey_resolution_failed" });
12009
+ throw this.initError;
12010
+ }
12011
+ }
12012
+ /** Apply resolved credentials and create the Supabase client. */
12013
+ applyResolvedConfig(resolved) {
12014
+ this.config = {
12015
+ ...this.config,
12016
+ projectId: resolved.projectId,
12017
+ supabaseUrl: resolved.supabaseUrl,
12018
+ supabaseAnonKey: resolved.supabaseAnonKey
12019
+ };
12020
+ this.supabase = createClient(resolved.supabaseUrl, resolved.supabaseAnonKey);
12021
+ this.initialized = true;
12022
+ this.initOfflineQueue();
12023
+ }
12024
+ /** Initialize offline queue if configured. Shared by both init paths. */
12025
+ initOfflineQueue() {
12026
+ if (this.config.offlineQueue?.enabled) {
11951
12027
  this._queue = new OfflineQueue({
11952
12028
  enabled: true,
11953
- maxItems: config.offlineQueue.maxItems,
11954
- maxRetries: config.offlineQueue.maxRetries
12029
+ maxItems: this.config.offlineQueue.maxItems,
12030
+ maxRetries: this.config.offlineQueue.maxRetries
11955
12031
  });
11956
12032
  this.registerQueueHandlers();
11957
12033
  }
11958
12034
  }
12035
+ /** Read cached config from localStorage if available and not expired. */
12036
+ readConfigCache(apiKey) {
12037
+ if (typeof localStorage === "undefined") return null;
12038
+ try {
12039
+ const key = CONFIG_CACHE_PREFIX + this.hashKey(apiKey);
12040
+ const raw = localStorage.getItem(key);
12041
+ if (!raw) return null;
12042
+ const cached = JSON.parse(raw);
12043
+ if (Date.now() - cached.cachedAt > CONFIG_CACHE_TTL_MS) {
12044
+ localStorage.removeItem(key);
12045
+ return null;
12046
+ }
12047
+ return cached;
12048
+ } catch {
12049
+ return null;
12050
+ }
12051
+ }
12052
+ /** Write resolved config to localStorage cache. */
12053
+ writeConfigCache(apiKey, data) {
12054
+ if (typeof localStorage === "undefined") return;
12055
+ try {
12056
+ const key = CONFIG_CACHE_PREFIX + this.hashKey(apiKey);
12057
+ const cached = { ...data, cachedAt: Date.now() };
12058
+ localStorage.setItem(key, JSON.stringify(cached));
12059
+ } catch {
12060
+ }
12061
+ }
12062
+ /** Simple string hash for cache keys — avoids storing raw API keys. */
12063
+ hashKey(apiKey) {
12064
+ let hash = 0;
12065
+ for (let i = 0; i < apiKey.length; i++) {
12066
+ hash = (hash << 5) - hash + apiKey.charCodeAt(i) | 0;
12067
+ }
12068
+ return hash.toString(36);
12069
+ }
12070
+ /** Ensure the client is initialized before making requests. */
12071
+ async ensureReady() {
12072
+ if (this.initialized) return;
12073
+ await this.pendingInit;
12074
+ if (this.initError) throw this.initError;
12075
+ }
11959
12076
  // ── Offline Queue ─────────────────────────────────────────
11960
12077
  /**
11961
12078
  * Access the offline queue (if enabled).
@@ -12111,6 +12228,7 @@ var BugBearClient = class {
12111
12228
  * Get current user info from host app or BugBear's own auth
12112
12229
  */
12113
12230
  async getCurrentUserInfo() {
12231
+ await this.ensureReady();
12114
12232
  if (this.config.getCurrentUser) {
12115
12233
  return await this.config.getCurrentUser();
12116
12234
  }
@@ -12330,6 +12448,7 @@ var BugBearClient = class {
12330
12448
  */
12331
12449
  async getAssignment(assignmentId) {
12332
12450
  try {
12451
+ await this.ensureReady();
12333
12452
  const { data, error } = await this.supabase.from("test_assignments").select(`
12334
12453
  id,
12335
12454
  status,
@@ -12401,6 +12520,7 @@ var BugBearClient = class {
12401
12520
  */
12402
12521
  async updateAssignmentStatus(assignmentId, status, options) {
12403
12522
  try {
12523
+ await this.ensureReady();
12404
12524
  const { data: currentAssignment, error: fetchError } = await this.supabase.from("test_assignments").select("status, started_at").eq("id", assignmentId).single();
12405
12525
  if (fetchError || !currentAssignment) {
12406
12526
  console.error("BugBear: Assignment not found", {
@@ -12487,6 +12607,7 @@ var BugBearClient = class {
12487
12607
  */
12488
12608
  async reopenAssignment(assignmentId) {
12489
12609
  try {
12610
+ await this.ensureReady();
12490
12611
  const { data: current, error: fetchError } = await this.supabase.from("test_assignments").select("status").eq("id", assignmentId).single();
12491
12612
  if (fetchError || !current) {
12492
12613
  return { success: false, error: "Assignment not found" };
@@ -12526,6 +12647,7 @@ var BugBearClient = class {
12526
12647
  actualNotes = notes;
12527
12648
  }
12528
12649
  try {
12650
+ await this.ensureReady();
12529
12651
  const updateData = {
12530
12652
  status: "skipped",
12531
12653
  skip_reason: actualReason,
@@ -12695,6 +12817,7 @@ var BugBearClient = class {
12695
12817
  */
12696
12818
  async getTesterInfo() {
12697
12819
  try {
12820
+ await this.ensureReady();
12698
12821
  const userInfo = await this.getCurrentUserInfo();
12699
12822
  if (!userInfo?.email) return null;
12700
12823
  if (!this.isValidEmail(userInfo.email)) {
@@ -12986,6 +13109,7 @@ var BugBearClient = class {
12986
13109
  */
12987
13110
  async isQAEnabled() {
12988
13111
  try {
13112
+ await this.ensureReady();
12989
13113
  const { data, error } = await this.supabase.rpc("check_qa_enabled", {
12990
13114
  p_project_id: this.config.projectId
12991
13115
  });
@@ -13017,6 +13141,7 @@ var BugBearClient = class {
13017
13141
  */
13018
13142
  async uploadScreenshot(file, filename, bucket = "screenshots") {
13019
13143
  try {
13144
+ await this.ensureReady();
13020
13145
  const contentType = file.type || "image/png";
13021
13146
  const ext = contentType.includes("png") ? "png" : "jpg";
13022
13147
  const name = filename || `screenshot-${Date.now()}.${ext}`;
@@ -13057,6 +13182,7 @@ var BugBearClient = class {
13057
13182
  */
13058
13183
  async uploadImageFromUri(uri, filename, bucket = "screenshots") {
13059
13184
  try {
13185
+ await this.ensureReady();
13060
13186
  const response = await fetch(uri);
13061
13187
  const blob = await response.blob();
13062
13188
  const contentType = blob.type || "image/jpeg";
@@ -13151,6 +13277,7 @@ var BugBearClient = class {
13151
13277
  */
13152
13278
  async getFixRequests(options) {
13153
13279
  try {
13280
+ await this.ensureReady();
13154
13281
  let query = this.supabase.from("fix_requests").select("*").eq("project_id", this.config.projectId).order("created_at", { ascending: false }).limit(options?.limit || 20);
13155
13282
  if (options?.status) {
13156
13283
  query = query.eq("status", options.status);
@@ -13222,6 +13349,7 @@ var BugBearClient = class {
13222
13349
  */
13223
13350
  async getThreadMessages(threadId) {
13224
13351
  try {
13352
+ await this.ensureReady();
13225
13353
  const { data, error } = await this.supabase.from("discussion_messages").select(`
13226
13354
  id,
13227
13355
  thread_id,
@@ -13424,6 +13552,7 @@ var BugBearClient = class {
13424
13552
  */
13425
13553
  async endSession(sessionId, options = {}) {
13426
13554
  try {
13555
+ await this.ensureReady();
13427
13556
  const { data, error } = await this.supabase.rpc("end_qa_session", {
13428
13557
  p_session_id: sessionId,
13429
13558
  p_notes: options.notes || null,
@@ -13461,6 +13590,7 @@ var BugBearClient = class {
13461
13590
  */
13462
13591
  async getSession(sessionId) {
13463
13592
  try {
13593
+ await this.ensureReady();
13464
13594
  const { data, error } = await this.supabase.from("qa_sessions").select("*").eq("id", sessionId).single();
13465
13595
  if (error || !data) return null;
13466
13596
  return this.transformSession(data);
@@ -13492,6 +13622,7 @@ var BugBearClient = class {
13492
13622
  */
13493
13623
  async addFinding(sessionId, options) {
13494
13624
  try {
13625
+ await this.ensureReady();
13495
13626
  const { data, error } = await this.supabase.rpc("add_session_finding", {
13496
13627
  p_session_id: sessionId,
13497
13628
  p_type: options.type,
@@ -13522,6 +13653,7 @@ var BugBearClient = class {
13522
13653
  */
13523
13654
  async getSessionFindings(sessionId) {
13524
13655
  try {
13656
+ await this.ensureReady();
13525
13657
  const { data, error } = await this.supabase.from("qa_findings").select("*").eq("session_id", sessionId).order("created_at", { ascending: true }).limit(100);
13526
13658
  if (error) {
13527
13659
  console.error("BugBear: Failed to fetch findings", formatPgError(error));
@@ -13538,6 +13670,7 @@ var BugBearClient = class {
13538
13670
  */
13539
13671
  async convertFindingToBug(findingId) {
13540
13672
  try {
13673
+ await this.ensureReady();
13541
13674
  const { data, error } = await this.supabase.rpc("convert_finding_to_bug", {
13542
13675
  p_finding_id: findingId
13543
13676
  });
@@ -13557,6 +13690,7 @@ var BugBearClient = class {
13557
13690
  */
13558
13691
  async dismissFinding(findingId, reason) {
13559
13692
  try {
13693
+ await this.ensureReady();
13560
13694
  const { error } = await this.supabase.from("qa_findings").update({
13561
13695
  dismissed: true,
13562
13696
  dismissed_reason: reason || null,
@@ -15108,9 +15242,9 @@ function TestListScreen({ nav }) {
15108
15242
  /* @__PURE__ */ import_react6.default.createElement(import_react_native6.Text, { style: [styles3.trackBtnText, isActive && { color: track.color, fontWeight: "600" }] }, track.icon, " ", track.name)
15109
15243
  );
15110
15244
  })), /* @__PURE__ */ import_react6.default.createElement(import_react_native6.View, { style: styles3.sortGroup }, [
15111
- { key: "priority", label: "\u2195" },
15112
- { key: "recent", label: "\u{1F550}" },
15113
- { key: "alpha", label: "AZ" }
15245
+ { key: "priority", label: "\u2195 Priority" },
15246
+ { key: "recent", label: "\u{1F550} Recent" },
15247
+ { key: "alpha", label: "A-Z" }
15114
15248
  ].map((s2) => /* @__PURE__ */ import_react6.default.createElement(
15115
15249
  import_react_native6.TouchableOpacity,
15116
15250
  {
@@ -16327,8 +16461,9 @@ var styles13 = import_react_native16.StyleSheet.create({
16327
16461
  // src/widget/screens/IssueListScreen.tsx
16328
16462
  var import_react18 = __toESM(require("react"));
16329
16463
  var import_react_native17 = require("react-native");
16464
+ var CATEGORIES = ["open", "done", "reopened"];
16330
16465
  var CATEGORY_CONFIG = {
16331
- open: { label: "Open Issues", accent: "#f97316", emptyIcon: "\u2705", emptyText: "No open issues" },
16466
+ open: { label: "Open", accent: "#f97316", emptyIcon: "\u2705", emptyText: "No open issues" },
16332
16467
  done: { label: "Done", accent: "#22c55e", emptyIcon: "\u{1F389}", emptyText: "No completed issues yet" },
16333
16468
  reopened: { label: "Reopened", accent: "#ef4444", emptyIcon: "\u{1F44D}", emptyText: "No reopened issues" }
16334
16469
  };
@@ -16338,11 +16473,20 @@ var SEVERITY_COLORS = {
16338
16473
  medium: "#eab308",
16339
16474
  low: "#71717a"
16340
16475
  };
16476
+ var SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
16341
16477
  function IssueListScreen({ nav, category }) {
16342
16478
  const { client } = useBugBear();
16479
+ const [activeCategory, setActiveCategory] = (0, import_react18.useState)(category);
16343
16480
  const [issues, setIssues] = (0, import_react18.useState)([]);
16344
16481
  const [loading, setLoading] = (0, import_react18.useState)(true);
16345
- const config = CATEGORY_CONFIG[category];
16482
+ const [counts, setCounts] = (0, import_react18.useState)(null);
16483
+ const [sortMode, setSortMode] = (0, import_react18.useState)("severity");
16484
+ const config = CATEGORY_CONFIG[activeCategory];
16485
+ (0, import_react18.useEffect)(() => {
16486
+ if (!client) return;
16487
+ client.getIssueCounts().then(setCounts).catch(() => {
16488
+ });
16489
+ }, [client]);
16346
16490
  (0, import_react18.useEffect)(() => {
16347
16491
  let cancelled = false;
16348
16492
  setLoading(true);
@@ -16352,7 +16496,7 @@ function IssueListScreen({ nav, category }) {
16352
16496
  return;
16353
16497
  }
16354
16498
  try {
16355
- const data = await client.getIssues(category);
16499
+ const data = await client.getIssues(activeCategory);
16356
16500
  if (!cancelled) {
16357
16501
  setIssues(data);
16358
16502
  }
@@ -16367,14 +16511,63 @@ function IssueListScreen({ nav, category }) {
16367
16511
  return () => {
16368
16512
  cancelled = true;
16369
16513
  };
16370
- }, [client, category]);
16371
- if (loading) {
16372
- return /* @__PURE__ */ import_react18.default.createElement(IssueListScreenSkeleton, null);
16373
- }
16374
- if (issues.length === 0) {
16375
- return /* @__PURE__ */ import_react18.default.createElement(import_react_native17.View, { style: styles14.emptyContainer }, /* @__PURE__ */ import_react18.default.createElement(import_react_native17.Text, { style: styles14.emptyIcon }, config.emptyIcon), /* @__PURE__ */ import_react18.default.createElement(import_react_native17.Text, { style: styles14.emptyText }, config.emptyText));
16376
- }
16377
- return /* @__PURE__ */ import_react18.default.createElement(import_react_native17.View, null, issues.map((issue) => /* @__PURE__ */ import_react18.default.createElement(
16514
+ }, [client, activeCategory]);
16515
+ const sortedIssues = (0, import_react18.useMemo)(() => {
16516
+ const sorted = [...issues];
16517
+ if (sortMode === "severity") {
16518
+ sorted.sort((a, b) => (SEVERITY_ORDER[a.severity || "low"] ?? 4) - (SEVERITY_ORDER[b.severity || "low"] ?? 4));
16519
+ } else {
16520
+ sorted.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
16521
+ }
16522
+ return sorted;
16523
+ }, [issues, sortMode]);
16524
+ return /* @__PURE__ */ import_react18.default.createElement(import_react_native17.View, null, /* @__PURE__ */ import_react18.default.createElement(import_react_native17.View, { style: styles14.tabBar }, CATEGORIES.map((cat) => {
16525
+ const catConfig = CATEGORY_CONFIG[cat];
16526
+ const isActive = activeCategory === cat;
16527
+ const count = counts?.[cat];
16528
+ return /* @__PURE__ */ import_react18.default.createElement(
16529
+ import_react_native17.TouchableOpacity,
16530
+ {
16531
+ key: cat,
16532
+ style: [
16533
+ styles14.tab,
16534
+ { borderBottomColor: isActive ? catConfig.accent : "transparent" }
16535
+ ],
16536
+ onPress: () => setActiveCategory(cat),
16537
+ activeOpacity: 0.7
16538
+ },
16539
+ /* @__PURE__ */ import_react18.default.createElement(import_react_native17.Text, { style: [
16540
+ styles14.tabLabel,
16541
+ isActive && { fontWeight: "600", color: colors.textPrimary }
16542
+ ] }, catConfig.label),
16543
+ count !== void 0 && /* @__PURE__ */ import_react18.default.createElement(import_react_native17.View, { style: [
16544
+ styles14.countBadge,
16545
+ {
16546
+ backgroundColor: isActive ? catConfig.accent + "18" : colors.card
16547
+ }
16548
+ ] }, /* @__PURE__ */ import_react18.default.createElement(import_react_native17.Text, { style: [
16549
+ styles14.countText,
16550
+ { color: isActive ? catConfig.accent : colors.textDim }
16551
+ ] }, count))
16552
+ );
16553
+ })), /* @__PURE__ */ import_react18.default.createElement(import_react_native17.View, { style: styles14.sortRow }, [
16554
+ { key: "severity", label: "Severity" },
16555
+ { key: "recent", label: "Recent" }
16556
+ ].map((s2) => /* @__PURE__ */ import_react18.default.createElement(
16557
+ import_react_native17.TouchableOpacity,
16558
+ {
16559
+ key: s2.key,
16560
+ style: [
16561
+ styles14.sortBtn,
16562
+ sortMode === s2.key && styles14.sortBtnActive
16563
+ ],
16564
+ onPress: () => setSortMode(s2.key)
16565
+ },
16566
+ /* @__PURE__ */ import_react18.default.createElement(import_react_native17.Text, { style: [
16567
+ styles14.sortBtnText,
16568
+ sortMode === s2.key && styles14.sortBtnTextActive
16569
+ ] }, s2.label)
16570
+ ))), loading ? /* @__PURE__ */ import_react18.default.createElement(IssueListScreenSkeleton, null) : sortedIssues.length === 0 ? /* @__PURE__ */ import_react18.default.createElement(import_react_native17.View, { style: styles14.emptyContainer }, /* @__PURE__ */ import_react18.default.createElement(import_react_native17.Text, { style: styles14.emptyIcon }, config.emptyIcon), /* @__PURE__ */ import_react18.default.createElement(import_react_native17.Text, { style: styles14.emptyText }, config.emptyText)) : sortedIssues.map((issue) => /* @__PURE__ */ import_react18.default.createElement(
16378
16571
  import_react_native17.TouchableOpacity,
16379
16572
  {
16380
16573
  key: issue.id,
@@ -16384,11 +16577,69 @@ function IssueListScreen({ nav, category }) {
16384
16577
  },
16385
16578
  /* @__PURE__ */ import_react18.default.createElement(import_react_native17.View, { style: styles14.topRow }, issue.severity && /* @__PURE__ */ import_react18.default.createElement(import_react_native17.View, { style: [styles14.severityDot, { backgroundColor: SEVERITY_COLORS[issue.severity] || colors.textDim }] }), /* @__PURE__ */ import_react18.default.createElement(import_react_native17.Text, { style: styles14.issueTitle, numberOfLines: 1 }, issue.title)),
16386
16579
  /* @__PURE__ */ import_react18.default.createElement(import_react_native17.View, { style: styles14.bottomRow }, issue.route && /* @__PURE__ */ import_react18.default.createElement(import_react_native17.Text, { style: styles14.routeText, numberOfLines: 1 }, issue.route), /* @__PURE__ */ import_react18.default.createElement(import_react_native17.Text, { style: styles14.timeText }, formatRelativeTime(issue.updatedAt))),
16387
- category === "done" && issue.verifiedByName && /* @__PURE__ */ import_react18.default.createElement(import_react_native17.View, { style: styles14.verifiedBadge }, /* @__PURE__ */ import_react18.default.createElement(import_react_native17.Text, { style: styles14.verifiedBadgeText }, "\u2714", " Verified by ", issue.verifiedByName)),
16388
- category === "reopened" && issue.originalBugTitle && /* @__PURE__ */ import_react18.default.createElement(import_react_native17.View, { style: styles14.reopenedBadge }, /* @__PURE__ */ import_react18.default.createElement(import_react_native17.Text, { style: styles14.reopenedBadgeText, numberOfLines: 1 }, "\u{1F504}", " Retest of: ", issue.originalBugTitle))
16580
+ activeCategory === "done" && issue.verifiedByName && /* @__PURE__ */ import_react18.default.createElement(import_react_native17.View, { style: styles14.verifiedBadge }, /* @__PURE__ */ import_react18.default.createElement(import_react_native17.Text, { style: styles14.verifiedBadgeText }, "\u2714", " Verified by ", issue.verifiedByName)),
16581
+ activeCategory === "reopened" && issue.originalBugTitle && /* @__PURE__ */ import_react18.default.createElement(import_react_native17.View, { style: styles14.reopenedBadge }, /* @__PURE__ */ import_react18.default.createElement(import_react_native17.Text, { style: styles14.reopenedBadgeText, numberOfLines: 1 }, "\u{1F504}", " Retest of: ", issue.originalBugTitle))
16389
16582
  )));
16390
16583
  }
16391
16584
  var styles14 = import_react_native17.StyleSheet.create({
16585
+ tabBar: {
16586
+ flexDirection: "row",
16587
+ borderBottomWidth: 1,
16588
+ borderBottomColor: colors.border,
16589
+ marginBottom: 8
16590
+ },
16591
+ tab: {
16592
+ flex: 1,
16593
+ flexDirection: "row",
16594
+ alignItems: "center",
16595
+ justifyContent: "center",
16596
+ gap: 6,
16597
+ paddingVertical: 8,
16598
+ paddingHorizontal: 4,
16599
+ borderBottomWidth: 2
16600
+ },
16601
+ tabLabel: {
16602
+ fontSize: 12,
16603
+ fontWeight: "400",
16604
+ color: colors.textMuted
16605
+ },
16606
+ countBadge: {
16607
+ borderRadius: 8,
16608
+ paddingHorizontal: 6,
16609
+ paddingVertical: 1,
16610
+ minWidth: 18,
16611
+ alignItems: "center"
16612
+ },
16613
+ countText: {
16614
+ fontSize: 10,
16615
+ fontWeight: "600"
16616
+ },
16617
+ sortRow: {
16618
+ flexDirection: "row",
16619
+ justifyContent: "flex-end",
16620
+ gap: 2,
16621
+ marginBottom: 8
16622
+ },
16623
+ sortBtn: {
16624
+ paddingHorizontal: 8,
16625
+ paddingVertical: 3,
16626
+ borderRadius: 6,
16627
+ borderWidth: 1,
16628
+ borderColor: "transparent"
16629
+ },
16630
+ sortBtnActive: {
16631
+ backgroundColor: colors.card,
16632
+ borderColor: colors.border
16633
+ },
16634
+ sortBtnText: {
16635
+ fontSize: 10,
16636
+ fontWeight: "400",
16637
+ color: colors.textMuted
16638
+ },
16639
+ sortBtnTextActive: {
16640
+ fontWeight: "600",
16641
+ color: colors.textPrimary
16642
+ },
16392
16643
  emptyContainer: {
16393
16644
  alignItems: "center",
16394
16645
  paddingVertical: 40
@@ -16844,7 +17095,7 @@ function BugBearButton({
16844
17095
  behavior: import_react_native19.Platform.OS === "ios" ? "padding" : "height",
16845
17096
  style: styles16.modalOverlay
16846
17097
  },
16847
- /* @__PURE__ */ import_react20.default.createElement(import_react_native19.View, { style: styles16.modalContainer }, /* @__PURE__ */ import_react20.default.createElement(import_react_native19.View, { style: styles16.header }, /* @__PURE__ */ import_react20.default.createElement(import_react_native19.View, { style: styles16.headerLeft }, canGoBack ? /* @__PURE__ */ import_react20.default.createElement(import_react_native19.View, { style: styles16.headerNavRow }, /* @__PURE__ */ import_react20.default.createElement(import_react_native19.TouchableOpacity, { onPress: () => nav.pop(), style: styles16.backButton }, /* @__PURE__ */ import_react20.default.createElement(import_react_native19.Text, { style: styles16.backText }, "\u2190 Back")), /* @__PURE__ */ import_react20.default.createElement(import_react_native19.TouchableOpacity, { onPress: () => nav.reset(), style: styles16.homeButton }, /* @__PURE__ */ import_react20.default.createElement(import_react_native19.Text, { style: styles16.homeText }, "\u{1F3E0}"))) : /* @__PURE__ */ import_react20.default.createElement(import_react_native19.View, { style: styles16.headerTitleRow }, /* @__PURE__ */ import_react20.default.createElement(import_react_native19.Text, { style: styles16.headerTitle }, "BugBear"), testerInfo && /* @__PURE__ */ import_react20.default.createElement(import_react_native19.TouchableOpacity, { onPress: () => push({ name: "PROFILE" }) }, /* @__PURE__ */ import_react20.default.createElement(import_react_native19.Text, { style: styles16.headerName }, testerInfo.name, " \u270E")))), getHeaderTitle() ? /* @__PURE__ */ import_react20.default.createElement(import_react_native19.Text, { style: styles16.headerScreenTitle, numberOfLines: 1 }, getHeaderTitle()) : null, /* @__PURE__ */ import_react20.default.createElement(import_react_native19.TouchableOpacity, { onPress: handleClose, style: styles16.closeButton }, /* @__PURE__ */ import_react20.default.createElement(import_react_native19.Text, { style: styles16.closeText }, "\u2715"))), /* @__PURE__ */ import_react20.default.createElement(
17098
+ /* @__PURE__ */ import_react20.default.createElement(import_react_native19.View, { style: styles16.modalContainer }, /* @__PURE__ */ import_react20.default.createElement(import_react_native19.View, { style: styles16.header }, /* @__PURE__ */ import_react20.default.createElement(import_react_native19.View, { style: styles16.headerLeft }, canGoBack ? /* @__PURE__ */ import_react20.default.createElement(import_react_native19.TouchableOpacity, { onPress: () => nav.pop(), style: styles16.backButton }, /* @__PURE__ */ import_react20.default.createElement(import_react_native19.Text, { style: styles16.backText }, "\u2190 Back")) : /* @__PURE__ */ import_react20.default.createElement(import_react_native19.View, { style: styles16.headerTitleRow }, /* @__PURE__ */ import_react20.default.createElement(import_react_native19.Text, { style: styles16.headerTitle }, "BugBear"), testerInfo && /* @__PURE__ */ import_react20.default.createElement(import_react_native19.TouchableOpacity, { onPress: () => push({ name: "PROFILE" }) }, /* @__PURE__ */ import_react20.default.createElement(import_react_native19.Text, { style: styles16.headerName }, testerInfo.name, " \u270E")))), getHeaderTitle() ? /* @__PURE__ */ import_react20.default.createElement(import_react_native19.Text, { style: styles16.headerScreenTitle, numberOfLines: 1 }, getHeaderTitle()) : null, /* @__PURE__ */ import_react20.default.createElement(import_react_native19.View, { style: styles16.headerActions }, currentScreen.name !== "HOME" && /* @__PURE__ */ import_react20.default.createElement(import_react_native19.TouchableOpacity, { onPress: () => nav.reset(), style: styles16.homeButton }, /* @__PURE__ */ import_react20.default.createElement(import_react_native19.Text, { style: styles16.homeIcon }, "\u2302")), /* @__PURE__ */ import_react20.default.createElement(import_react_native19.TouchableOpacity, { onPress: handleClose, style: styles16.closeButton }, /* @__PURE__ */ import_react20.default.createElement(import_react_native19.Text, { style: styles16.closeText }, "\u2715")))), /* @__PURE__ */ import_react20.default.createElement(
16848
17099
  import_react_native19.ScrollView,
16849
17100
  {
16850
17101
  style: styles16.content,
@@ -16945,11 +17196,6 @@ var styles16 = import_react_native19.StyleSheet.create({
16945
17196
  flex: 1,
16946
17197
  textAlign: "center"
16947
17198
  },
16948
- headerNavRow: {
16949
- flexDirection: "row",
16950
- alignItems: "center",
16951
- gap: 8
16952
- },
16953
17199
  backButton: {
16954
17200
  paddingVertical: 2,
16955
17201
  paddingRight: 4
@@ -16959,12 +17205,25 @@ var styles16 = import_react_native19.StyleSheet.create({
16959
17205
  color: colors.blue,
16960
17206
  fontWeight: "500"
16961
17207
  },
17208
+ headerActions: {
17209
+ flexDirection: "row",
17210
+ alignItems: "center",
17211
+ gap: 6
17212
+ },
16962
17213
  homeButton: {
16963
- paddingVertical: 2,
16964
- paddingHorizontal: 6
17214
+ width: 28,
17215
+ height: 28,
17216
+ borderRadius: 14,
17217
+ backgroundColor: "#1e3a5f",
17218
+ borderWidth: 1,
17219
+ borderColor: "rgba(59, 130, 246, 0.25)",
17220
+ justifyContent: "center",
17221
+ alignItems: "center"
16965
17222
  },
16966
- homeText: {
16967
- fontSize: 16
17223
+ homeIcon: {
17224
+ fontSize: 16,
17225
+ color: "#60a5fa",
17226
+ marginTop: -1
16968
17227
  },
16969
17228
  closeButton: {
16970
17229
  width: 32,
package/dist/index.mjs CHANGED
@@ -11507,6 +11507,7 @@ var ContextCaptureManager = class {
11507
11507
  this.networkRequests = [];
11508
11508
  this.navigationHistory = [];
11509
11509
  this.originalConsole = {};
11510
+ this.fetchHost = null;
11510
11511
  this.isCapturing = false;
11511
11512
  }
11512
11513
  /**
@@ -11529,8 +11530,9 @@ var ContextCaptureManager = class {
11529
11530
  if (this.originalConsole.warn) console.warn = this.originalConsole.warn;
11530
11531
  if (this.originalConsole.error) console.error = this.originalConsole.error;
11531
11532
  if (this.originalConsole.info) console.info = this.originalConsole.info;
11532
- if (this.originalFetch && typeof window !== "undefined") {
11533
- window.fetch = this.originalFetch;
11533
+ if (this.originalFetch && this.fetchHost) {
11534
+ this.fetchHost.fetch = this.originalFetch;
11535
+ this.fetchHost = null;
11534
11536
  }
11535
11537
  if (typeof window !== "undefined" && typeof history !== "undefined") {
11536
11538
  if (this.originalPushState) {
@@ -11639,15 +11641,19 @@ var ContextCaptureManager = class {
11639
11641
  });
11640
11642
  }
11641
11643
  captureFetch() {
11642
- if (typeof window === "undefined" || typeof fetch === "undefined" || typeof document === "undefined") return;
11643
- this.originalFetch = window.fetch;
11644
+ if (typeof fetch === "undefined") return;
11645
+ const host = typeof window !== "undefined" && typeof window.fetch === "function" ? window : typeof globalThis !== "undefined" && typeof globalThis.fetch === "function" ? globalThis : null;
11646
+ if (!host) return;
11647
+ const canCloneResponse = typeof document !== "undefined";
11648
+ this.fetchHost = host;
11649
+ this.originalFetch = host.fetch;
11644
11650
  const self2 = this;
11645
- window.fetch = async function(input, init) {
11651
+ host.fetch = async function(input, init) {
11646
11652
  const startTime = Date.now();
11647
11653
  const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
11648
11654
  const method = init?.method || "GET";
11649
11655
  try {
11650
- const response = await self2.originalFetch.call(window, input, init);
11656
+ const response = await self2.originalFetch.call(host, input, init);
11651
11657
  const requestEntry = {
11652
11658
  method,
11653
11659
  url: url.slice(0, 200),
@@ -11656,7 +11662,7 @@ var ContextCaptureManager = class {
11656
11662
  duration: Date.now() - startTime,
11657
11663
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
11658
11664
  };
11659
- if (response.status >= 400) {
11665
+ if (canCloneResponse && response.status >= 400) {
11660
11666
  try {
11661
11667
  const cloned = response.clone();
11662
11668
  const body = await cloned.text();
@@ -11900,29 +11906,140 @@ var formatPgError = (e) => {
11900
11906
  const { message, code, details, hint } = e;
11901
11907
  return { message, code, details, hint };
11902
11908
  };
11909
+ var DEFAULT_API_BASE_URL = "https://app.bugbear.ai";
11910
+ var CONFIG_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
11911
+ var CONFIG_CACHE_PREFIX = "bugbear_config_";
11903
11912
  var BugBearClient = class {
11904
11913
  constructor(config) {
11905
11914
  this.navigationHistory = [];
11906
11915
  this.reportSubmitInFlight = false;
11907
11916
  this._queue = null;
11908
11917
  this.realtimeChannels = [];
11909
- if (!config.supabaseUrl) {
11910
- throw new Error("BugBear: supabaseUrl is required. Get it from your BugBear project settings.");
11911
- }
11912
- if (!config.supabaseAnonKey) {
11913
- throw new Error("BugBear: supabaseAnonKey is required. Get it from your BugBear project settings.");
11914
- }
11918
+ this.initialized = false;
11919
+ this.initError = null;
11915
11920
  this.config = config;
11916
- this.supabase = createClient(config.supabaseUrl, config.supabaseAnonKey);
11917
- if (config.offlineQueue?.enabled) {
11921
+ if (config.apiKey) {
11922
+ this.pendingInit = this.resolveFromApiKey(config.apiKey);
11923
+ } else if (config.supabaseUrl && config.supabaseAnonKey) {
11924
+ if (!config.projectId) {
11925
+ throw new Error(
11926
+ "BugBear: projectId is required when using explicit Supabase credentials. Tip: Use apiKey instead for simpler setup \u2014 it resolves everything automatically."
11927
+ );
11928
+ }
11929
+ this.supabase = createClient(config.supabaseUrl, config.supabaseAnonKey);
11930
+ this.initialized = true;
11931
+ this.pendingInit = Promise.resolve();
11932
+ this.initOfflineQueue();
11933
+ } else {
11934
+ throw new Error(
11935
+ "BugBear: Missing configuration. Provide either:\n \u2022 apiKey (recommended) \u2014 resolves everything automatically\n \u2022 projectId + supabaseUrl + supabaseAnonKey \u2014 explicit credentials\n\nGet your API key at https://app.bugbear.ai/settings/projects"
11936
+ );
11937
+ }
11938
+ }
11939
+ /** Whether the client is ready for requests. */
11940
+ get isReady() {
11941
+ return this.initialized;
11942
+ }
11943
+ /** Wait until the client is ready. Throws if initialization failed. */
11944
+ async ready() {
11945
+ await this.pendingInit;
11946
+ if (this.initError) throw this.initError;
11947
+ }
11948
+ /**
11949
+ * Resolve Supabase credentials from a BugBear API key.
11950
+ * Checks localStorage cache first, falls back to /api/v1/config.
11951
+ */
11952
+ async resolveFromApiKey(apiKey) {
11953
+ try {
11954
+ const cached = this.readConfigCache(apiKey);
11955
+ if (cached) {
11956
+ this.applyResolvedConfig(cached);
11957
+ return;
11958
+ }
11959
+ const baseUrl = (this.config.apiBaseUrl || DEFAULT_API_BASE_URL).replace(/\/$/, "");
11960
+ const response = await fetch(`${baseUrl}/api/v1/config`, {
11961
+ headers: { Authorization: `Bearer ${apiKey}` }
11962
+ });
11963
+ if (!response.ok) {
11964
+ const body = await response.json().catch(() => ({}));
11965
+ const message = body.error || `HTTP ${response.status}`;
11966
+ throw new Error(
11967
+ `BugBear: Invalid API key \u2014 ${message}. Get yours at https://app.bugbear.ai/settings/projects`
11968
+ );
11969
+ }
11970
+ const data = await response.json();
11971
+ this.writeConfigCache(apiKey, data);
11972
+ this.applyResolvedConfig(data);
11973
+ } catch (err) {
11974
+ this.initError = err instanceof Error ? err : new Error(String(err));
11975
+ this.config.onError?.(this.initError, { context: "apikey_resolution_failed" });
11976
+ throw this.initError;
11977
+ }
11978
+ }
11979
+ /** Apply resolved credentials and create the Supabase client. */
11980
+ applyResolvedConfig(resolved) {
11981
+ this.config = {
11982
+ ...this.config,
11983
+ projectId: resolved.projectId,
11984
+ supabaseUrl: resolved.supabaseUrl,
11985
+ supabaseAnonKey: resolved.supabaseAnonKey
11986
+ };
11987
+ this.supabase = createClient(resolved.supabaseUrl, resolved.supabaseAnonKey);
11988
+ this.initialized = true;
11989
+ this.initOfflineQueue();
11990
+ }
11991
+ /** Initialize offline queue if configured. Shared by both init paths. */
11992
+ initOfflineQueue() {
11993
+ if (this.config.offlineQueue?.enabled) {
11918
11994
  this._queue = new OfflineQueue({
11919
11995
  enabled: true,
11920
- maxItems: config.offlineQueue.maxItems,
11921
- maxRetries: config.offlineQueue.maxRetries
11996
+ maxItems: this.config.offlineQueue.maxItems,
11997
+ maxRetries: this.config.offlineQueue.maxRetries
11922
11998
  });
11923
11999
  this.registerQueueHandlers();
11924
12000
  }
11925
12001
  }
12002
+ /** Read cached config from localStorage if available and not expired. */
12003
+ readConfigCache(apiKey) {
12004
+ if (typeof localStorage === "undefined") return null;
12005
+ try {
12006
+ const key = CONFIG_CACHE_PREFIX + this.hashKey(apiKey);
12007
+ const raw = localStorage.getItem(key);
12008
+ if (!raw) return null;
12009
+ const cached = JSON.parse(raw);
12010
+ if (Date.now() - cached.cachedAt > CONFIG_CACHE_TTL_MS) {
12011
+ localStorage.removeItem(key);
12012
+ return null;
12013
+ }
12014
+ return cached;
12015
+ } catch {
12016
+ return null;
12017
+ }
12018
+ }
12019
+ /** Write resolved config to localStorage cache. */
12020
+ writeConfigCache(apiKey, data) {
12021
+ if (typeof localStorage === "undefined") return;
12022
+ try {
12023
+ const key = CONFIG_CACHE_PREFIX + this.hashKey(apiKey);
12024
+ const cached = { ...data, cachedAt: Date.now() };
12025
+ localStorage.setItem(key, JSON.stringify(cached));
12026
+ } catch {
12027
+ }
12028
+ }
12029
+ /** Simple string hash for cache keys — avoids storing raw API keys. */
12030
+ hashKey(apiKey) {
12031
+ let hash = 0;
12032
+ for (let i = 0; i < apiKey.length; i++) {
12033
+ hash = (hash << 5) - hash + apiKey.charCodeAt(i) | 0;
12034
+ }
12035
+ return hash.toString(36);
12036
+ }
12037
+ /** Ensure the client is initialized before making requests. */
12038
+ async ensureReady() {
12039
+ if (this.initialized) return;
12040
+ await this.pendingInit;
12041
+ if (this.initError) throw this.initError;
12042
+ }
11926
12043
  // ── Offline Queue ─────────────────────────────────────────
11927
12044
  /**
11928
12045
  * Access the offline queue (if enabled).
@@ -12078,6 +12195,7 @@ var BugBearClient = class {
12078
12195
  * Get current user info from host app or BugBear's own auth
12079
12196
  */
12080
12197
  async getCurrentUserInfo() {
12198
+ await this.ensureReady();
12081
12199
  if (this.config.getCurrentUser) {
12082
12200
  return await this.config.getCurrentUser();
12083
12201
  }
@@ -12297,6 +12415,7 @@ var BugBearClient = class {
12297
12415
  */
12298
12416
  async getAssignment(assignmentId) {
12299
12417
  try {
12418
+ await this.ensureReady();
12300
12419
  const { data, error } = await this.supabase.from("test_assignments").select(`
12301
12420
  id,
12302
12421
  status,
@@ -12368,6 +12487,7 @@ var BugBearClient = class {
12368
12487
  */
12369
12488
  async updateAssignmentStatus(assignmentId, status, options) {
12370
12489
  try {
12490
+ await this.ensureReady();
12371
12491
  const { data: currentAssignment, error: fetchError } = await this.supabase.from("test_assignments").select("status, started_at").eq("id", assignmentId).single();
12372
12492
  if (fetchError || !currentAssignment) {
12373
12493
  console.error("BugBear: Assignment not found", {
@@ -12454,6 +12574,7 @@ var BugBearClient = class {
12454
12574
  */
12455
12575
  async reopenAssignment(assignmentId) {
12456
12576
  try {
12577
+ await this.ensureReady();
12457
12578
  const { data: current, error: fetchError } = await this.supabase.from("test_assignments").select("status").eq("id", assignmentId).single();
12458
12579
  if (fetchError || !current) {
12459
12580
  return { success: false, error: "Assignment not found" };
@@ -12493,6 +12614,7 @@ var BugBearClient = class {
12493
12614
  actualNotes = notes;
12494
12615
  }
12495
12616
  try {
12617
+ await this.ensureReady();
12496
12618
  const updateData = {
12497
12619
  status: "skipped",
12498
12620
  skip_reason: actualReason,
@@ -12662,6 +12784,7 @@ var BugBearClient = class {
12662
12784
  */
12663
12785
  async getTesterInfo() {
12664
12786
  try {
12787
+ await this.ensureReady();
12665
12788
  const userInfo = await this.getCurrentUserInfo();
12666
12789
  if (!userInfo?.email) return null;
12667
12790
  if (!this.isValidEmail(userInfo.email)) {
@@ -12953,6 +13076,7 @@ var BugBearClient = class {
12953
13076
  */
12954
13077
  async isQAEnabled() {
12955
13078
  try {
13079
+ await this.ensureReady();
12956
13080
  const { data, error } = await this.supabase.rpc("check_qa_enabled", {
12957
13081
  p_project_id: this.config.projectId
12958
13082
  });
@@ -12984,6 +13108,7 @@ var BugBearClient = class {
12984
13108
  */
12985
13109
  async uploadScreenshot(file, filename, bucket = "screenshots") {
12986
13110
  try {
13111
+ await this.ensureReady();
12987
13112
  const contentType = file.type || "image/png";
12988
13113
  const ext = contentType.includes("png") ? "png" : "jpg";
12989
13114
  const name = filename || `screenshot-${Date.now()}.${ext}`;
@@ -13024,6 +13149,7 @@ var BugBearClient = class {
13024
13149
  */
13025
13150
  async uploadImageFromUri(uri, filename, bucket = "screenshots") {
13026
13151
  try {
13152
+ await this.ensureReady();
13027
13153
  const response = await fetch(uri);
13028
13154
  const blob = await response.blob();
13029
13155
  const contentType = blob.type || "image/jpeg";
@@ -13118,6 +13244,7 @@ var BugBearClient = class {
13118
13244
  */
13119
13245
  async getFixRequests(options) {
13120
13246
  try {
13247
+ await this.ensureReady();
13121
13248
  let query = this.supabase.from("fix_requests").select("*").eq("project_id", this.config.projectId).order("created_at", { ascending: false }).limit(options?.limit || 20);
13122
13249
  if (options?.status) {
13123
13250
  query = query.eq("status", options.status);
@@ -13189,6 +13316,7 @@ var BugBearClient = class {
13189
13316
  */
13190
13317
  async getThreadMessages(threadId) {
13191
13318
  try {
13319
+ await this.ensureReady();
13192
13320
  const { data, error } = await this.supabase.from("discussion_messages").select(`
13193
13321
  id,
13194
13322
  thread_id,
@@ -13391,6 +13519,7 @@ var BugBearClient = class {
13391
13519
  */
13392
13520
  async endSession(sessionId, options = {}) {
13393
13521
  try {
13522
+ await this.ensureReady();
13394
13523
  const { data, error } = await this.supabase.rpc("end_qa_session", {
13395
13524
  p_session_id: sessionId,
13396
13525
  p_notes: options.notes || null,
@@ -13428,6 +13557,7 @@ var BugBearClient = class {
13428
13557
  */
13429
13558
  async getSession(sessionId) {
13430
13559
  try {
13560
+ await this.ensureReady();
13431
13561
  const { data, error } = await this.supabase.from("qa_sessions").select("*").eq("id", sessionId).single();
13432
13562
  if (error || !data) return null;
13433
13563
  return this.transformSession(data);
@@ -13459,6 +13589,7 @@ var BugBearClient = class {
13459
13589
  */
13460
13590
  async addFinding(sessionId, options) {
13461
13591
  try {
13592
+ await this.ensureReady();
13462
13593
  const { data, error } = await this.supabase.rpc("add_session_finding", {
13463
13594
  p_session_id: sessionId,
13464
13595
  p_type: options.type,
@@ -13489,6 +13620,7 @@ var BugBearClient = class {
13489
13620
  */
13490
13621
  async getSessionFindings(sessionId) {
13491
13622
  try {
13623
+ await this.ensureReady();
13492
13624
  const { data, error } = await this.supabase.from("qa_findings").select("*").eq("session_id", sessionId).order("created_at", { ascending: true }).limit(100);
13493
13625
  if (error) {
13494
13626
  console.error("BugBear: Failed to fetch findings", formatPgError(error));
@@ -13505,6 +13637,7 @@ var BugBearClient = class {
13505
13637
  */
13506
13638
  async convertFindingToBug(findingId) {
13507
13639
  try {
13640
+ await this.ensureReady();
13508
13641
  const { data, error } = await this.supabase.rpc("convert_finding_to_bug", {
13509
13642
  p_finding_id: findingId
13510
13643
  });
@@ -13524,6 +13657,7 @@ var BugBearClient = class {
13524
13657
  */
13525
13658
  async dismissFinding(findingId, reason) {
13526
13659
  try {
13660
+ await this.ensureReady();
13527
13661
  const { error } = await this.supabase.from("qa_findings").update({
13528
13662
  dismissed: true,
13529
13663
  dismissed_reason: reason || null,
@@ -15090,9 +15224,9 @@ function TestListScreen({ nav }) {
15090
15224
  /* @__PURE__ */ React5.createElement(Text3, { style: [styles3.trackBtnText, isActive && { color: track.color, fontWeight: "600" }] }, track.icon, " ", track.name)
15091
15225
  );
15092
15226
  })), /* @__PURE__ */ React5.createElement(View4, { style: styles3.sortGroup }, [
15093
- { key: "priority", label: "\u2195" },
15094
- { key: "recent", label: "\u{1F550}" },
15095
- { key: "alpha", label: "AZ" }
15227
+ { key: "priority", label: "\u2195 Priority" },
15228
+ { key: "recent", label: "\u{1F550} Recent" },
15229
+ { key: "alpha", label: "A-Z" }
15096
15230
  ].map((s2) => /* @__PURE__ */ React5.createElement(
15097
15231
  TouchableOpacity3,
15098
15232
  {
@@ -16307,10 +16441,11 @@ var styles13 = StyleSheet15.create({
16307
16441
  });
16308
16442
 
16309
16443
  // src/widget/screens/IssueListScreen.tsx
16310
- import React16, { useState as useState11, useEffect as useEffect10 } from "react";
16444
+ import React16, { useState as useState11, useEffect as useEffect10, useMemo as useMemo3 } from "react";
16311
16445
  import { View as View15, Text as Text14, TouchableOpacity as TouchableOpacity13, StyleSheet as StyleSheet16 } from "react-native";
16446
+ var CATEGORIES = ["open", "done", "reopened"];
16312
16447
  var CATEGORY_CONFIG = {
16313
- open: { label: "Open Issues", accent: "#f97316", emptyIcon: "\u2705", emptyText: "No open issues" },
16448
+ open: { label: "Open", accent: "#f97316", emptyIcon: "\u2705", emptyText: "No open issues" },
16314
16449
  done: { label: "Done", accent: "#22c55e", emptyIcon: "\u{1F389}", emptyText: "No completed issues yet" },
16315
16450
  reopened: { label: "Reopened", accent: "#ef4444", emptyIcon: "\u{1F44D}", emptyText: "No reopened issues" }
16316
16451
  };
@@ -16320,11 +16455,20 @@ var SEVERITY_COLORS = {
16320
16455
  medium: "#eab308",
16321
16456
  low: "#71717a"
16322
16457
  };
16458
+ var SEVERITY_ORDER = { critical: 0, high: 1, medium: 2, low: 3 };
16323
16459
  function IssueListScreen({ nav, category }) {
16324
16460
  const { client } = useBugBear();
16461
+ const [activeCategory, setActiveCategory] = useState11(category);
16325
16462
  const [issues, setIssues] = useState11([]);
16326
16463
  const [loading, setLoading] = useState11(true);
16327
- const config = CATEGORY_CONFIG[category];
16464
+ const [counts, setCounts] = useState11(null);
16465
+ const [sortMode, setSortMode] = useState11("severity");
16466
+ const config = CATEGORY_CONFIG[activeCategory];
16467
+ useEffect10(() => {
16468
+ if (!client) return;
16469
+ client.getIssueCounts().then(setCounts).catch(() => {
16470
+ });
16471
+ }, [client]);
16328
16472
  useEffect10(() => {
16329
16473
  let cancelled = false;
16330
16474
  setLoading(true);
@@ -16334,7 +16478,7 @@ function IssueListScreen({ nav, category }) {
16334
16478
  return;
16335
16479
  }
16336
16480
  try {
16337
- const data = await client.getIssues(category);
16481
+ const data = await client.getIssues(activeCategory);
16338
16482
  if (!cancelled) {
16339
16483
  setIssues(data);
16340
16484
  }
@@ -16349,14 +16493,63 @@ function IssueListScreen({ nav, category }) {
16349
16493
  return () => {
16350
16494
  cancelled = true;
16351
16495
  };
16352
- }, [client, category]);
16353
- if (loading) {
16354
- return /* @__PURE__ */ React16.createElement(IssueListScreenSkeleton, null);
16355
- }
16356
- if (issues.length === 0) {
16357
- return /* @__PURE__ */ React16.createElement(View15, { style: styles14.emptyContainer }, /* @__PURE__ */ React16.createElement(Text14, { style: styles14.emptyIcon }, config.emptyIcon), /* @__PURE__ */ React16.createElement(Text14, { style: styles14.emptyText }, config.emptyText));
16358
- }
16359
- return /* @__PURE__ */ React16.createElement(View15, null, issues.map((issue) => /* @__PURE__ */ React16.createElement(
16496
+ }, [client, activeCategory]);
16497
+ const sortedIssues = useMemo3(() => {
16498
+ const sorted = [...issues];
16499
+ if (sortMode === "severity") {
16500
+ sorted.sort((a, b) => (SEVERITY_ORDER[a.severity || "low"] ?? 4) - (SEVERITY_ORDER[b.severity || "low"] ?? 4));
16501
+ } else {
16502
+ sorted.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
16503
+ }
16504
+ return sorted;
16505
+ }, [issues, sortMode]);
16506
+ return /* @__PURE__ */ React16.createElement(View15, null, /* @__PURE__ */ React16.createElement(View15, { style: styles14.tabBar }, CATEGORIES.map((cat) => {
16507
+ const catConfig = CATEGORY_CONFIG[cat];
16508
+ const isActive = activeCategory === cat;
16509
+ const count = counts?.[cat];
16510
+ return /* @__PURE__ */ React16.createElement(
16511
+ TouchableOpacity13,
16512
+ {
16513
+ key: cat,
16514
+ style: [
16515
+ styles14.tab,
16516
+ { borderBottomColor: isActive ? catConfig.accent : "transparent" }
16517
+ ],
16518
+ onPress: () => setActiveCategory(cat),
16519
+ activeOpacity: 0.7
16520
+ },
16521
+ /* @__PURE__ */ React16.createElement(Text14, { style: [
16522
+ styles14.tabLabel,
16523
+ isActive && { fontWeight: "600", color: colors.textPrimary }
16524
+ ] }, catConfig.label),
16525
+ count !== void 0 && /* @__PURE__ */ React16.createElement(View15, { style: [
16526
+ styles14.countBadge,
16527
+ {
16528
+ backgroundColor: isActive ? catConfig.accent + "18" : colors.card
16529
+ }
16530
+ ] }, /* @__PURE__ */ React16.createElement(Text14, { style: [
16531
+ styles14.countText,
16532
+ { color: isActive ? catConfig.accent : colors.textDim }
16533
+ ] }, count))
16534
+ );
16535
+ })), /* @__PURE__ */ React16.createElement(View15, { style: styles14.sortRow }, [
16536
+ { key: "severity", label: "Severity" },
16537
+ { key: "recent", label: "Recent" }
16538
+ ].map((s2) => /* @__PURE__ */ React16.createElement(
16539
+ TouchableOpacity13,
16540
+ {
16541
+ key: s2.key,
16542
+ style: [
16543
+ styles14.sortBtn,
16544
+ sortMode === s2.key && styles14.sortBtnActive
16545
+ ],
16546
+ onPress: () => setSortMode(s2.key)
16547
+ },
16548
+ /* @__PURE__ */ React16.createElement(Text14, { style: [
16549
+ styles14.sortBtnText,
16550
+ sortMode === s2.key && styles14.sortBtnTextActive
16551
+ ] }, s2.label)
16552
+ ))), loading ? /* @__PURE__ */ React16.createElement(IssueListScreenSkeleton, null) : sortedIssues.length === 0 ? /* @__PURE__ */ React16.createElement(View15, { style: styles14.emptyContainer }, /* @__PURE__ */ React16.createElement(Text14, { style: styles14.emptyIcon }, config.emptyIcon), /* @__PURE__ */ React16.createElement(Text14, { style: styles14.emptyText }, config.emptyText)) : sortedIssues.map((issue) => /* @__PURE__ */ React16.createElement(
16360
16553
  TouchableOpacity13,
16361
16554
  {
16362
16555
  key: issue.id,
@@ -16366,11 +16559,69 @@ function IssueListScreen({ nav, category }) {
16366
16559
  },
16367
16560
  /* @__PURE__ */ React16.createElement(View15, { style: styles14.topRow }, issue.severity && /* @__PURE__ */ React16.createElement(View15, { style: [styles14.severityDot, { backgroundColor: SEVERITY_COLORS[issue.severity] || colors.textDim }] }), /* @__PURE__ */ React16.createElement(Text14, { style: styles14.issueTitle, numberOfLines: 1 }, issue.title)),
16368
16561
  /* @__PURE__ */ React16.createElement(View15, { style: styles14.bottomRow }, issue.route && /* @__PURE__ */ React16.createElement(Text14, { style: styles14.routeText, numberOfLines: 1 }, issue.route), /* @__PURE__ */ React16.createElement(Text14, { style: styles14.timeText }, formatRelativeTime(issue.updatedAt))),
16369
- category === "done" && issue.verifiedByName && /* @__PURE__ */ React16.createElement(View15, { style: styles14.verifiedBadge }, /* @__PURE__ */ React16.createElement(Text14, { style: styles14.verifiedBadgeText }, "\u2714", " Verified by ", issue.verifiedByName)),
16370
- category === "reopened" && issue.originalBugTitle && /* @__PURE__ */ React16.createElement(View15, { style: styles14.reopenedBadge }, /* @__PURE__ */ React16.createElement(Text14, { style: styles14.reopenedBadgeText, numberOfLines: 1 }, "\u{1F504}", " Retest of: ", issue.originalBugTitle))
16562
+ activeCategory === "done" && issue.verifiedByName && /* @__PURE__ */ React16.createElement(View15, { style: styles14.verifiedBadge }, /* @__PURE__ */ React16.createElement(Text14, { style: styles14.verifiedBadgeText }, "\u2714", " Verified by ", issue.verifiedByName)),
16563
+ activeCategory === "reopened" && issue.originalBugTitle && /* @__PURE__ */ React16.createElement(View15, { style: styles14.reopenedBadge }, /* @__PURE__ */ React16.createElement(Text14, { style: styles14.reopenedBadgeText, numberOfLines: 1 }, "\u{1F504}", " Retest of: ", issue.originalBugTitle))
16371
16564
  )));
16372
16565
  }
16373
16566
  var styles14 = StyleSheet16.create({
16567
+ tabBar: {
16568
+ flexDirection: "row",
16569
+ borderBottomWidth: 1,
16570
+ borderBottomColor: colors.border,
16571
+ marginBottom: 8
16572
+ },
16573
+ tab: {
16574
+ flex: 1,
16575
+ flexDirection: "row",
16576
+ alignItems: "center",
16577
+ justifyContent: "center",
16578
+ gap: 6,
16579
+ paddingVertical: 8,
16580
+ paddingHorizontal: 4,
16581
+ borderBottomWidth: 2
16582
+ },
16583
+ tabLabel: {
16584
+ fontSize: 12,
16585
+ fontWeight: "400",
16586
+ color: colors.textMuted
16587
+ },
16588
+ countBadge: {
16589
+ borderRadius: 8,
16590
+ paddingHorizontal: 6,
16591
+ paddingVertical: 1,
16592
+ minWidth: 18,
16593
+ alignItems: "center"
16594
+ },
16595
+ countText: {
16596
+ fontSize: 10,
16597
+ fontWeight: "600"
16598
+ },
16599
+ sortRow: {
16600
+ flexDirection: "row",
16601
+ justifyContent: "flex-end",
16602
+ gap: 2,
16603
+ marginBottom: 8
16604
+ },
16605
+ sortBtn: {
16606
+ paddingHorizontal: 8,
16607
+ paddingVertical: 3,
16608
+ borderRadius: 6,
16609
+ borderWidth: 1,
16610
+ borderColor: "transparent"
16611
+ },
16612
+ sortBtnActive: {
16613
+ backgroundColor: colors.card,
16614
+ borderColor: colors.border
16615
+ },
16616
+ sortBtnText: {
16617
+ fontSize: 10,
16618
+ fontWeight: "400",
16619
+ color: colors.textMuted
16620
+ },
16621
+ sortBtnTextActive: {
16622
+ fontWeight: "600",
16623
+ color: colors.textPrimary
16624
+ },
16374
16625
  emptyContainer: {
16375
16626
  alignItems: "center",
16376
16627
  paddingVertical: 40
@@ -16826,7 +17077,7 @@ function BugBearButton({
16826
17077
  behavior: Platform5.OS === "ios" ? "padding" : "height",
16827
17078
  style: styles16.modalOverlay
16828
17079
  },
16829
- /* @__PURE__ */ React18.createElement(View17, { style: styles16.modalContainer }, /* @__PURE__ */ React18.createElement(View17, { style: styles16.header }, /* @__PURE__ */ React18.createElement(View17, { style: styles16.headerLeft }, canGoBack ? /* @__PURE__ */ React18.createElement(View17, { style: styles16.headerNavRow }, /* @__PURE__ */ React18.createElement(TouchableOpacity15, { onPress: () => nav.pop(), style: styles16.backButton }, /* @__PURE__ */ React18.createElement(Text16, { style: styles16.backText }, "\u2190 Back")), /* @__PURE__ */ React18.createElement(TouchableOpacity15, { onPress: () => nav.reset(), style: styles16.homeButton }, /* @__PURE__ */ React18.createElement(Text16, { style: styles16.homeText }, "\u{1F3E0}"))) : /* @__PURE__ */ React18.createElement(View17, { style: styles16.headerTitleRow }, /* @__PURE__ */ React18.createElement(Text16, { style: styles16.headerTitle }, "BugBear"), testerInfo && /* @__PURE__ */ React18.createElement(TouchableOpacity15, { onPress: () => push({ name: "PROFILE" }) }, /* @__PURE__ */ React18.createElement(Text16, { style: styles16.headerName }, testerInfo.name, " \u270E")))), getHeaderTitle() ? /* @__PURE__ */ React18.createElement(Text16, { style: styles16.headerScreenTitle, numberOfLines: 1 }, getHeaderTitle()) : null, /* @__PURE__ */ React18.createElement(TouchableOpacity15, { onPress: handleClose, style: styles16.closeButton }, /* @__PURE__ */ React18.createElement(Text16, { style: styles16.closeText }, "\u2715"))), /* @__PURE__ */ React18.createElement(
17080
+ /* @__PURE__ */ React18.createElement(View17, { style: styles16.modalContainer }, /* @__PURE__ */ React18.createElement(View17, { style: styles16.header }, /* @__PURE__ */ React18.createElement(View17, { style: styles16.headerLeft }, canGoBack ? /* @__PURE__ */ React18.createElement(TouchableOpacity15, { onPress: () => nav.pop(), style: styles16.backButton }, /* @__PURE__ */ React18.createElement(Text16, { style: styles16.backText }, "\u2190 Back")) : /* @__PURE__ */ React18.createElement(View17, { style: styles16.headerTitleRow }, /* @__PURE__ */ React18.createElement(Text16, { style: styles16.headerTitle }, "BugBear"), testerInfo && /* @__PURE__ */ React18.createElement(TouchableOpacity15, { onPress: () => push({ name: "PROFILE" }) }, /* @__PURE__ */ React18.createElement(Text16, { style: styles16.headerName }, testerInfo.name, " \u270E")))), getHeaderTitle() ? /* @__PURE__ */ React18.createElement(Text16, { style: styles16.headerScreenTitle, numberOfLines: 1 }, getHeaderTitle()) : null, /* @__PURE__ */ React18.createElement(View17, { style: styles16.headerActions }, currentScreen.name !== "HOME" && /* @__PURE__ */ React18.createElement(TouchableOpacity15, { onPress: () => nav.reset(), style: styles16.homeButton }, /* @__PURE__ */ React18.createElement(Text16, { style: styles16.homeIcon }, "\u2302")), /* @__PURE__ */ React18.createElement(TouchableOpacity15, { onPress: handleClose, style: styles16.closeButton }, /* @__PURE__ */ React18.createElement(Text16, { style: styles16.closeText }, "\u2715")))), /* @__PURE__ */ React18.createElement(
16830
17081
  ScrollView3,
16831
17082
  {
16832
17083
  style: styles16.content,
@@ -16927,11 +17178,6 @@ var styles16 = StyleSheet18.create({
16927
17178
  flex: 1,
16928
17179
  textAlign: "center"
16929
17180
  },
16930
- headerNavRow: {
16931
- flexDirection: "row",
16932
- alignItems: "center",
16933
- gap: 8
16934
- },
16935
17181
  backButton: {
16936
17182
  paddingVertical: 2,
16937
17183
  paddingRight: 4
@@ -16941,12 +17187,25 @@ var styles16 = StyleSheet18.create({
16941
17187
  color: colors.blue,
16942
17188
  fontWeight: "500"
16943
17189
  },
17190
+ headerActions: {
17191
+ flexDirection: "row",
17192
+ alignItems: "center",
17193
+ gap: 6
17194
+ },
16944
17195
  homeButton: {
16945
- paddingVertical: 2,
16946
- paddingHorizontal: 6
17196
+ width: 28,
17197
+ height: 28,
17198
+ borderRadius: 14,
17199
+ backgroundColor: "#1e3a5f",
17200
+ borderWidth: 1,
17201
+ borderColor: "rgba(59, 130, 246, 0.25)",
17202
+ justifyContent: "center",
17203
+ alignItems: "center"
16947
17204
  },
16948
- homeText: {
16949
- fontSize: 16
17205
+ homeIcon: {
17206
+ fontSize: 16,
17207
+ color: "#60a5fa",
17208
+ marginTop: -1
16950
17209
  },
16951
17210
  closeButton: {
16952
17211
  width: 32,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bbearai/react-native",
3
- "version": "0.6.3",
3
+ "version": "0.7.1",
4
4
  "description": "BugBear React Native components for mobile apps",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -51,7 +51,7 @@
51
51
  }
52
52
  },
53
53
  "devDependencies": {
54
- "@bbearai/core": "^0.5.0",
54
+ "@bbearai/core": "^0.6.0",
55
55
  "@eslint/js": "^9.39.2",
56
56
  "@testing-library/jest-dom": "^6.9.1",
57
57
  "@testing-library/react": "^16.3.2",