@bbearai/react-native 0.8.2 → 0.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -12197,10 +12197,14 @@ 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;
12203
12204
  this.initError = null;
12205
+ this._presenceSessionId = null;
12206
+ this._presenceInterval = null;
12207
+ this._presencePaused = false;
12204
12208
  this.config = config;
12205
12209
  if (config.apiKey) {
12206
12210
  this.pendingInit = this.resolveFromApiKey(config.apiKey);
@@ -12274,6 +12278,10 @@ var BugBearClient = class {
12274
12278
  this.initOfflineQueue();
12275
12279
  this.initMonitoring();
12276
12280
  }
12281
+ /** Cache key scoped to the active project. */
12282
+ get sessionCacheKey() {
12283
+ return `bugbear_session_${this.config.projectId ?? "unknown"}`;
12284
+ }
12277
12285
  /** Initialize offline queue if configured. Shared by both init paths. */
12278
12286
  initOfflineQueue() {
12279
12287
  if (this.config.offlineQueue?.enabled) {
@@ -12337,6 +12345,26 @@ var BugBearClient = class {
12337
12345
  await this.pendingInit;
12338
12346
  if (this.initError) throw this.initError;
12339
12347
  }
12348
+ /**
12349
+ * Fire-and-forget call to a dashboard notification endpoint.
12350
+ * Only works when apiKey is configured (needed for API auth).
12351
+ * Failures are silently ignored — notifications are best-effort.
12352
+ */
12353
+ async notifyDashboard(path, body) {
12354
+ if (!this.config.apiKey) return;
12355
+ try {
12356
+ const baseUrl = (this.config.apiBaseUrl || DEFAULT_API_BASE_URL).replace(/\/$/, "");
12357
+ await fetch(`${baseUrl}/api/v1/notifications/${path}`, {
12358
+ method: "POST",
12359
+ headers: {
12360
+ "Content-Type": "application/json",
12361
+ "Authorization": `Bearer ${this.config.apiKey}`
12362
+ },
12363
+ body: JSON.stringify(body)
12364
+ });
12365
+ } catch {
12366
+ }
12367
+ }
12340
12368
  // ── Offline Queue ─────────────────────────────────────────
12341
12369
  /**
12342
12370
  * Access the offline queue (if enabled).
@@ -12363,6 +12391,48 @@ var BugBearClient = class {
12363
12391
  }
12364
12392
  await this._queue.load();
12365
12393
  }
12394
+ // ── Session Cache ──────────────────────────────────────────
12395
+ /**
12396
+ * Swap the session cache storage adapter (for React Native — pass AsyncStorage).
12397
+ * Must be called before getCachedSession() for persistence across app kills.
12398
+ * Web callers don't need this — LocalStorageAdapter is the default.
12399
+ */
12400
+ setSessionStorage(adapter) {
12401
+ this._sessionStorage = adapter;
12402
+ }
12403
+ /**
12404
+ * Cache the active QA session locally for instant restore on app restart.
12405
+ * Pass null to clear the cache (e.g. after ending a session).
12406
+ */
12407
+ async cacheSession(session) {
12408
+ try {
12409
+ if (session) {
12410
+ await this._sessionStorage.setItem(this.sessionCacheKey, JSON.stringify(session));
12411
+ } else {
12412
+ await this._sessionStorage.removeItem(this.sessionCacheKey);
12413
+ }
12414
+ } catch {
12415
+ }
12416
+ }
12417
+ /**
12418
+ * Retrieve the cached QA session. Returns null if no cache, if stale (>24h),
12419
+ * or if parsing fails. The DB fetch in initializeBugBear() is the source of truth.
12420
+ */
12421
+ async getCachedSession() {
12422
+ try {
12423
+ const raw = await this._sessionStorage.getItem(this.sessionCacheKey);
12424
+ if (!raw) return null;
12425
+ const session = JSON.parse(raw);
12426
+ const age = Date.now() - new Date(session.startedAt).getTime();
12427
+ if (age > 24 * 60 * 60 * 1e3) {
12428
+ await this._sessionStorage.removeItem(this.sessionCacheKey);
12429
+ return null;
12430
+ }
12431
+ return session;
12432
+ } catch {
12433
+ return null;
12434
+ }
12435
+ }
12366
12436
  registerQueueHandlers() {
12367
12437
  if (!this._queue) return;
12368
12438
  this._queue.registerHandler("report", async (payload) => {
@@ -12380,6 +12450,11 @@ var BugBearClient = class {
12380
12450
  if (error) return { success: false, error: error.message };
12381
12451
  return { success: true };
12382
12452
  });
12453
+ this._queue.registerHandler("email_capture", async (payload) => {
12454
+ const { error } = await this.supabase.from("email_captures").insert(payload).select("id").single();
12455
+ if (error) return { success: false, error: error.message };
12456
+ return { success: true };
12457
+ });
12383
12458
  }
12384
12459
  // ── Realtime Subscriptions ─────────────────────────────────
12385
12460
  /** Whether realtime is enabled in config. */
@@ -12567,6 +12642,8 @@ var BugBearClient = class {
12567
12642
  if (this.config.onReportSubmitted) {
12568
12643
  this.config.onReportSubmitted(report);
12569
12644
  }
12645
+ this.notifyDashboard("report", { reportId: data.id }).catch(() => {
12646
+ });
12570
12647
  return { success: true, reportId: data.id };
12571
12648
  } catch (err) {
12572
12649
  const message = err instanceof Error ? err.message : "Unknown error";
@@ -12579,6 +12656,44 @@ var BugBearClient = class {
12579
12656
  this.reportSubmitInFlight = false;
12580
12657
  }
12581
12658
  }
12659
+ /**
12660
+ * Capture an email for QA testing.
12661
+ * Called by the email interceptor — not typically called directly.
12662
+ */
12663
+ async captureEmail(payload) {
12664
+ try {
12665
+ await this.ready();
12666
+ if (!payload.subject || !payload.to || payload.to.length === 0) {
12667
+ return { success: false, error: "subject and to are required" };
12668
+ }
12669
+ const record = {
12670
+ project_id: this.config.projectId,
12671
+ to_addresses: payload.to,
12672
+ from_address: payload.from || null,
12673
+ subject: payload.subject,
12674
+ html_content: payload.html || null,
12675
+ text_content: payload.text || null,
12676
+ template_id: payload.templateId || null,
12677
+ metadata: payload.metadata || {},
12678
+ capture_mode: payload.captureMode,
12679
+ was_delivered: payload.wasDelivered,
12680
+ delivery_status: payload.wasDelivered ? "sent" : "pending"
12681
+ };
12682
+ const { data, error } = await this.supabase.from("email_captures").insert(record).select("id").single();
12683
+ if (error) {
12684
+ if (this._queue && isNetworkError(error.message)) {
12685
+ await this._queue.enqueue("email_capture", record);
12686
+ return { success: false, queued: true, error: "Queued \u2014 will send when online" };
12687
+ }
12688
+ console.error("BugBear: Failed to capture email", error.message);
12689
+ return { success: false, error: error.message };
12690
+ }
12691
+ return { success: true, captureId: data.id };
12692
+ } catch (err) {
12693
+ const message = err instanceof Error ? err.message : "Unknown error";
12694
+ return { success: false, error: message };
12695
+ }
12696
+ }
12582
12697
  /**
12583
12698
  * Get assigned tests for current user
12584
12699
  * First looks up the tester by email, then fetches their assignments
@@ -13200,6 +13315,32 @@ var BugBearClient = class {
13200
13315
  return [];
13201
13316
  }
13202
13317
  }
13318
+ /**
13319
+ * Reopen a done issue that the tester believes isn't actually fixed.
13320
+ * Transitions the report from a done status back to 'confirmed'.
13321
+ */
13322
+ async reopenReport(reportId, reason) {
13323
+ try {
13324
+ const testerInfo = await this.getTesterInfo();
13325
+ if (!testerInfo) return { success: false, error: "Not authenticated as tester" };
13326
+ const { data, error } = await this.supabase.rpc("reopen_report", {
13327
+ p_report_id: reportId,
13328
+ p_tester_id: testerInfo.id,
13329
+ p_reason: reason
13330
+ });
13331
+ if (error) {
13332
+ console.error("BugBear: Failed to reopen report", formatPgError(error));
13333
+ return { success: false, error: error.message };
13334
+ }
13335
+ if (!data?.success) {
13336
+ return { success: false, error: data?.error || "Failed to reopen report" };
13337
+ }
13338
+ return { success: true };
13339
+ } catch (err) {
13340
+ console.error("BugBear: Error reopening report", err);
13341
+ return { success: false, error: "Unexpected error" };
13342
+ }
13343
+ }
13203
13344
  /**
13204
13345
  * Basic email format validation (defense in depth)
13205
13346
  */
@@ -13709,6 +13850,7 @@ var BugBearClient = class {
13709
13850
  lastMessageAt: row.last_message_at,
13710
13851
  createdAt: row.created_at,
13711
13852
  unreadCount: Number(row.unread_count) || 0,
13853
+ reporterName: row.reporter_name || void 0,
13712
13854
  lastMessage: row.last_message_preview ? {
13713
13855
  id: "",
13714
13856
  threadId: row.thread_id,
@@ -13786,7 +13928,7 @@ var BugBearClient = class {
13786
13928
  insertData.attachments = safeAttachments;
13787
13929
  }
13788
13930
  }
13789
- const { error } = await this.supabase.from("discussion_messages").insert(insertData);
13931
+ const { data: msgData, error } = await this.supabase.from("discussion_messages").insert(insertData).select("id").single();
13790
13932
  if (error) {
13791
13933
  if (this._queue && isNetworkError(error.message)) {
13792
13934
  await this._queue.enqueue("message", insertData);
@@ -13795,6 +13937,10 @@ var BugBearClient = class {
13795
13937
  console.error("BugBear: Failed to send message", formatPgError(error));
13796
13938
  return false;
13797
13939
  }
13940
+ if (msgData?.id) {
13941
+ this.notifyDashboard("message", { threadId, messageId: msgData.id }).catch(() => {
13942
+ });
13943
+ }
13798
13944
  await this.markThreadAsRead(threadId);
13799
13945
  return true;
13800
13946
  } catch (err) {
@@ -13920,6 +14066,7 @@ var BugBearClient = class {
13920
14066
  if (!session) {
13921
14067
  return { success: false, error: "Session created but could not be fetched" };
13922
14068
  }
14069
+ await this.cacheSession(session);
13923
14070
  return { success: true, session };
13924
14071
  } catch (err) {
13925
14072
  const message = err instanceof Error ? err.message : "Unknown error";
@@ -13943,6 +14090,7 @@ var BugBearClient = class {
13943
14090
  return { success: false, error: error.message };
13944
14091
  }
13945
14092
  const session = this.transformSession(data);
14093
+ await this.cacheSession(null);
13946
14094
  return { success: true, session };
13947
14095
  } catch (err) {
13948
14096
  const message = err instanceof Error ? err.message : "Unknown error";
@@ -13958,8 +14106,13 @@ var BugBearClient = class {
13958
14106
  const testerInfo = await this.getTesterInfo();
13959
14107
  if (!testerInfo) return null;
13960
14108
  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);
14109
+ if (error || !data) {
14110
+ await this.cacheSession(null);
14111
+ return null;
14112
+ }
14113
+ const session = this.transformSession(data);
14114
+ await this.cacheSession(session);
14115
+ return session;
13963
14116
  } catch (err) {
13964
14117
  console.error("BugBear: Error fetching active session", err);
13965
14118
  return null;
@@ -14137,6 +14290,93 @@ var BugBearClient = class {
14137
14290
  updatedAt: data.updated_at
14138
14291
  };
14139
14292
  }
14293
+ // ─── Passive Presence Tracking ──────────────────────────────
14294
+ /** Current presence session ID (null if not tracking). */
14295
+ get presenceSessionId() {
14296
+ return this._presenceSessionId;
14297
+ }
14298
+ /**
14299
+ * Start passive presence tracking for this tester.
14300
+ * Idempotent — reuses an existing active session if one exists.
14301
+ */
14302
+ async startPresence(platform) {
14303
+ try {
14304
+ await this.ensureReady();
14305
+ const testerInfo = await this.getTesterInfo();
14306
+ if (!testerInfo) return null;
14307
+ const { data, error } = await this.supabase.rpc("upsert_tester_presence", {
14308
+ p_project_id: this.config.projectId,
14309
+ p_tester_id: testerInfo.id,
14310
+ p_platform: platform
14311
+ });
14312
+ if (error) {
14313
+ console.error("BugBear: Failed to start presence", formatPgError(error));
14314
+ return null;
14315
+ }
14316
+ this._presenceSessionId = data;
14317
+ this._presencePaused = false;
14318
+ this.startPresenceHeartbeat();
14319
+ return data;
14320
+ } catch (err) {
14321
+ console.error("BugBear: Error starting presence", err);
14322
+ return null;
14323
+ }
14324
+ }
14325
+ /** Gracefully end the current presence session. */
14326
+ async endPresence() {
14327
+ this.stopPresenceHeartbeat();
14328
+ if (!this._presenceSessionId) return;
14329
+ try {
14330
+ await this.supabase.rpc("end_tester_presence", {
14331
+ p_session_id: this._presenceSessionId
14332
+ });
14333
+ } catch {
14334
+ }
14335
+ this._presenceSessionId = null;
14336
+ }
14337
+ /** Pause heartbeat (tab hidden / app backgrounded). Sends one final beat. */
14338
+ pausePresence() {
14339
+ this._presencePaused = true;
14340
+ this.heartbeatPresence();
14341
+ }
14342
+ /** Resume heartbeat after pause. Restarts if session was cleaned up. */
14343
+ async resumePresence() {
14344
+ if (!this._presenceSessionId) return;
14345
+ this._presencePaused = false;
14346
+ try {
14347
+ const { data } = await this.supabase.rpc("heartbeat_tester_presence", {
14348
+ p_session_id: this._presenceSessionId
14349
+ });
14350
+ if (!data) {
14351
+ this._presenceSessionId = null;
14352
+ }
14353
+ } catch {
14354
+ this._presenceSessionId = null;
14355
+ }
14356
+ }
14357
+ async heartbeatPresence() {
14358
+ if (!this._presenceSessionId || this._presencePaused) return;
14359
+ try {
14360
+ const { data, error } = await this.supabase.rpc("heartbeat_tester_presence", {
14361
+ p_session_id: this._presenceSessionId
14362
+ });
14363
+ if (error || data === false) {
14364
+ this.stopPresenceHeartbeat();
14365
+ this._presenceSessionId = null;
14366
+ }
14367
+ } catch {
14368
+ }
14369
+ }
14370
+ startPresenceHeartbeat() {
14371
+ this.stopPresenceHeartbeat();
14372
+ this._presenceInterval = setInterval(() => this.heartbeatPresence(), 6e4);
14373
+ }
14374
+ stopPresenceHeartbeat() {
14375
+ if (this._presenceInterval) {
14376
+ clearInterval(this._presenceInterval);
14377
+ this._presenceInterval = null;
14378
+ }
14379
+ }
14140
14380
  };
14141
14381
  function createBugBear(config) {
14142
14382
  return new BugBearClient(config);
@@ -14271,6 +14511,9 @@ function setActiveColors(palette) {
14271
14511
  colors = palette;
14272
14512
  shared = createSharedStyles();
14273
14513
  }
14514
+ function withAlpha(hex, alpha) {
14515
+ return hex + Math.round(alpha * 255).toString(16).padStart(2, "0");
14516
+ }
14274
14517
  function createSharedStyles() {
14275
14518
  return StyleSheet.create({
14276
14519
  card: {
@@ -14477,6 +14720,7 @@ var BugBearContext = createContext({
14477
14720
  issueCounts: { open: 0, done: 0, reopened: 0 },
14478
14721
  refreshIssueCounts: async () => {
14479
14722
  },
14723
+ reopenReport: async () => ({ success: false }),
14480
14724
  queuedCount: 0,
14481
14725
  dashboardUrl: void 0,
14482
14726
  onError: void 0
@@ -14618,6 +14862,14 @@ function BugBearProvider({ config, children, appVersion, enabled = true }) {
14618
14862
  const counts = await client.getIssueCounts();
14619
14863
  setIssueCounts(counts);
14620
14864
  }, [client]);
14865
+ const reopenReport = useCallback(async (reportId, reason) => {
14866
+ if (!client) return { success: false, error: "Client not initialized" };
14867
+ const result = await client.reopenReport(reportId, reason);
14868
+ if (result.success) {
14869
+ await refreshIssueCounts();
14870
+ }
14871
+ return result;
14872
+ }, [client, refreshIssueCounts]);
14621
14873
  const initializeBugBear = useCallback(async (bugBearClient) => {
14622
14874
  setIsLoading(true);
14623
14875
  try {
@@ -14703,6 +14955,10 @@ function BugBearProvider({ config, children, appVersion, enabled = true }) {
14703
14955
  setClient(newClient);
14704
14956
  (async () => {
14705
14957
  try {
14958
+ const cachedSession = await newClient.getCachedSession();
14959
+ if (cachedSession) {
14960
+ setActiveSession(cachedSession);
14961
+ }
14706
14962
  await initializeBugBear(newClient);
14707
14963
  if (newClient.monitor && config.monitoring) {
14708
14964
  const getCurrentRoute = () => contextCapture.getCurrentRoute();
@@ -14760,6 +15016,28 @@ function BugBearProvider({ config, children, appVersion, enabled = true }) {
14760
15016
  }
14761
15017
  return () => subscription.remove();
14762
15018
  }, [client]);
15019
+ useEffect(() => {
15020
+ if (!client || !isTester) return;
15021
+ let mounted = true;
15022
+ const platform = Platform2.OS === "ios" ? "ios" : "android";
15023
+ client.startPresence(platform);
15024
+ const subscription = AppState.addEventListener("change", (nextState) => {
15025
+ if (nextState === "active") {
15026
+ client.resumePresence().then(() => {
15027
+ if (mounted && !client.presenceSessionId) {
15028
+ client.startPresence(platform);
15029
+ }
15030
+ });
15031
+ } else if (nextState === "background" || nextState === "inactive") {
15032
+ client.pausePresence();
15033
+ }
15034
+ });
15035
+ return () => {
15036
+ mounted = false;
15037
+ subscription.remove();
15038
+ client.endPresence();
15039
+ };
15040
+ }, [client, isTester]);
14763
15041
  useEffect(() => {
14764
15042
  if (!client || !isTester) return;
14765
15043
  if (widgetMode === "qa" && !isQAEnabled) return;
@@ -14837,6 +15115,7 @@ function BugBearProvider({ config, children, appVersion, enabled = true }) {
14837
15115
  // Issue tracking
14838
15116
  issueCounts,
14839
15117
  refreshIssueCounts,
15118
+ reopenReport,
14840
15119
  queuedCount,
14841
15120
  dashboardUrl: config.dashboardUrl,
14842
15121
  onError: config.onError
@@ -14854,21 +15133,21 @@ function BugBearProvider({ config, children, appVersion, enabled = true }) {
14854
15133
  }
14855
15134
 
14856
15135
  // src/BugBearButton.tsx
14857
- import React21, { useState as useState16, useRef as useRef6, useMemo as useMemo16 } from "react";
15136
+ import React21, { useState as useState18, useRef as useRef6, useMemo as useMemo16 } from "react";
14858
15137
  import {
14859
15138
  View as View21,
14860
15139
  Text as Text19,
14861
15140
  Image as Image4,
14862
15141
  TouchableOpacity as TouchableOpacity18,
14863
15142
  Modal as Modal3,
14864
- ScrollView as ScrollView3,
15143
+ ScrollView as ScrollView4,
14865
15144
  StyleSheet as StyleSheet21,
14866
15145
  Dimensions as Dimensions2,
14867
15146
  KeyboardAvoidingView,
14868
15147
  Platform as Platform5,
14869
15148
  PanResponder,
14870
15149
  Animated as Animated2,
14871
- ActivityIndicator as ActivityIndicator2,
15150
+ ActivityIndicator as ActivityIndicator3,
14872
15151
  Keyboard as Keyboard4
14873
15152
  } from "react-native";
14874
15153
 
@@ -15583,6 +15862,17 @@ function TestDetailScreen({ testId, nav }) {
15583
15862
  setShowSteps(true);
15584
15863
  setShowDetails(false);
15585
15864
  }, [displayedAssignment?.id]);
15865
+ useEffect4(() => {
15866
+ if (!client || !displayedAssignment || displayedAssignment.status !== "pending") return;
15867
+ let cancelled = false;
15868
+ (async () => {
15869
+ await client.updateAssignmentStatus(displayedAssignment.id, "in_progress");
15870
+ if (!cancelled) await refreshAssignments();
15871
+ })();
15872
+ return () => {
15873
+ cancelled = true;
15874
+ };
15875
+ }, [client, displayedAssignment?.id, displayedAssignment?.status, refreshAssignments]);
15586
15876
  useEffect4(() => {
15587
15877
  const active = displayedAssignment?.status === "in_progress" ? displayedAssignment : null;
15588
15878
  if (!active?.startedAt) {
@@ -15633,17 +15923,6 @@ function TestDetailScreen({ testId, nav }) {
15633
15923
  setIsSubmitting(false);
15634
15924
  }
15635
15925
  }, [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
15926
  const handleReopen = useCallback2(async () => {
15648
15927
  if (!client || !displayedAssignment || isSubmitting) return;
15649
15928
  Keyboard.dismiss();
@@ -15789,15 +16068,7 @@ function TestDetailScreen({ testId, nav }) {
15789
16068
  disabled: isSubmitting
15790
16069
  },
15791
16070
  /* @__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:"), [
16071
+ ))) : /* @__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
16072
  { reason: "blocked", label: "\u{1F6AB} Blocked by a bug" },
15802
16073
  { reason: "not_ready", label: "\u{1F6A7} Feature not ready" },
15803
16074
  { reason: "dependency", label: "\u{1F517} Needs another test first" },
@@ -15911,9 +16182,6 @@ function createStyles2() {
15911
16182
  completedLabel: { fontSize: 13, fontWeight: "600", color: colors.textSecondary },
15912
16183
  reopenBtn: { backgroundColor: colors.card, borderWidth: 1, borderColor: colors.blue },
15913
16184
  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
16185
  // Action buttons
15918
16186
  actionButtons: { flexDirection: "row", gap: 10, marginTop: 8 },
15919
16187
  actionBtn: { flex: 1, paddingVertical: 14, borderRadius: 12, alignItems: "center" },
@@ -16782,6 +17050,8 @@ function ReportScreen({ nav, prefill, autoCaptureUri, onAutoCaptureConsumed }) {
16782
17050
  const [severity, setSeverity] = useState8("medium");
16783
17051
  const [category, setCategory] = useState8(null);
16784
17052
  const [description, setDescription] = useState8("");
17053
+ const [myIssues, setMyIssues] = useState8([]);
17054
+ const [similarReports, setSimilarReports] = useState8([]);
16785
17055
  const [affectedScreen, setAffectedScreen] = useState8("");
16786
17056
  const [submitting, setSubmitting] = useState8(false);
16787
17057
  const [error, setError] = useState8(null);
@@ -16797,6 +17067,41 @@ function ReportScreen({ nav, prefill, autoCaptureUri, onAutoCaptureConsumed }) {
16797
17067
  }, [autoCaptureUri, onAutoCaptureConsumed]);
16798
17068
  const isRetestFailure = prefill?.type === "test_fail";
16799
17069
  const isBugType = reportType === "bug" || reportType === "test_fail";
17070
+ useEffect6(() => {
17071
+ if (!client?.getIssues) return;
17072
+ let cancelled = false;
17073
+ const load = async () => {
17074
+ try {
17075
+ const [open, done] = await Promise.all([
17076
+ client.getIssues("open"),
17077
+ client.getIssues("done")
17078
+ ]);
17079
+ if (!cancelled) setMyIssues([...open, ...done]);
17080
+ } catch {
17081
+ }
17082
+ };
17083
+ load();
17084
+ return () => {
17085
+ cancelled = true;
17086
+ };
17087
+ }, [client]);
17088
+ useEffect6(() => {
17089
+ if (description.length < 10 || myIssues.length === 0) {
17090
+ setSimilarReports([]);
17091
+ return;
17092
+ }
17093
+ const words = description.toLowerCase().split(/\s+/).filter((w) => w.length > 3);
17094
+ if (words.length === 0) {
17095
+ setSimilarReports([]);
17096
+ return;
17097
+ }
17098
+ const scored = myIssues.map((issue) => {
17099
+ const text = `${issue.title || ""} ${issue.description || ""}`.toLowerCase();
17100
+ const matches = words.filter((w) => text.includes(w)).length;
17101
+ return { issue, score: matches / words.length };
17102
+ }).filter((s2) => s2.score >= 0.3).sort((a, b) => b.score - a.score).slice(0, 3);
17103
+ setSimilarReports(scored.map((s2) => s2.issue));
17104
+ }, [description, myIssues]);
16800
17105
  useEffect6(() => {
16801
17106
  if (reportType === "feedback" || reportType === "suggestion") {
16802
17107
  setCategory("other");
@@ -16871,7 +17176,41 @@ function ReportScreen({ nav, prefill, autoCaptureUri, onAutoCaptureConsumed }) {
16871
17176
  numberOfLines: 4,
16872
17177
  textAlignVertical: "top"
16873
17178
  }
16874
- )), /* @__PURE__ */ React10.createElement(
17179
+ ), similarReports.length > 0 && /* @__PURE__ */ React10.createElement(View10, { style: {
17180
+ marginTop: 8,
17181
+ padding: 12,
17182
+ borderRadius: 8,
17183
+ backgroundColor: withAlpha(colors.orange, 0.1),
17184
+ borderWidth: 1,
17185
+ borderColor: withAlpha(colors.orange, 0.3)
17186
+ } }, /* @__PURE__ */ React10.createElement(Text8, { style: { fontSize: 12, fontWeight: "600", color: colors.orange, marginBottom: 8 } }, "Similar reports you've already filed:"), similarReports.map((issue) => /* @__PURE__ */ React10.createElement(
17187
+ TouchableOpacity8,
17188
+ {
17189
+ key: issue.id,
17190
+ onPress: () => nav.push({ name: "ISSUE_DETAIL", issue }),
17191
+ style: {
17192
+ flexDirection: "row",
17193
+ alignItems: "center",
17194
+ gap: 8,
17195
+ paddingVertical: 6
17196
+ }
17197
+ },
17198
+ /* @__PURE__ */ React10.createElement(View10, { style: {
17199
+ width: 8,
17200
+ height: 8,
17201
+ borderRadius: 4,
17202
+ backgroundColor: issue.severity === "critical" ? colors.red : issue.severity === "high" ? colors.orange : colors.yellow
17203
+ } }),
17204
+ /* @__PURE__ */ React10.createElement(
17205
+ Text8,
17206
+ {
17207
+ numberOfLines: 1,
17208
+ style: { fontSize: 12, color: colors.textPrimary, flex: 1 }
17209
+ },
17210
+ issue.title
17211
+ ),
17212
+ /* @__PURE__ */ React10.createElement(Text8, { style: { fontSize: 11, color: colors.textMuted } }, issue.status)
17213
+ )))), /* @__PURE__ */ React10.createElement(
16875
17214
  ImagePickerButtons,
16876
17215
  {
16877
17216
  images: images.images,
@@ -16927,7 +17266,41 @@ function ReportScreen({ nav, prefill, autoCaptureUri, onAutoCaptureConsumed }) {
16927
17266
  numberOfLines: 4,
16928
17267
  textAlignVertical: "top"
16929
17268
  }
16930
- )), isBugType && /* @__PURE__ */ React10.createElement(View10, { style: styles5.section }, /* @__PURE__ */ React10.createElement(Text8, { style: shared.label }, "Which screen?"), /* @__PURE__ */ React10.createElement(
17269
+ ), similarReports.length > 0 && /* @__PURE__ */ React10.createElement(View10, { style: {
17270
+ marginTop: 8,
17271
+ padding: 12,
17272
+ borderRadius: 8,
17273
+ backgroundColor: withAlpha(colors.orange, 0.1),
17274
+ borderWidth: 1,
17275
+ borderColor: withAlpha(colors.orange, 0.3)
17276
+ } }, /* @__PURE__ */ React10.createElement(Text8, { style: { fontSize: 12, fontWeight: "600", color: colors.orange, marginBottom: 8 } }, "Similar reports you've already filed:"), similarReports.map((issue) => /* @__PURE__ */ React10.createElement(
17277
+ TouchableOpacity8,
17278
+ {
17279
+ key: issue.id,
17280
+ onPress: () => nav.push({ name: "ISSUE_DETAIL", issue }),
17281
+ style: {
17282
+ flexDirection: "row",
17283
+ alignItems: "center",
17284
+ gap: 8,
17285
+ paddingVertical: 6
17286
+ }
17287
+ },
17288
+ /* @__PURE__ */ React10.createElement(View10, { style: {
17289
+ width: 8,
17290
+ height: 8,
17291
+ borderRadius: 4,
17292
+ backgroundColor: issue.severity === "critical" ? colors.red : issue.severity === "high" ? colors.orange : colors.yellow
17293
+ } }),
17294
+ /* @__PURE__ */ React10.createElement(
17295
+ Text8,
17296
+ {
17297
+ numberOfLines: 1,
17298
+ style: { fontSize: 12, color: colors.textPrimary, flex: 1 }
17299
+ },
17300
+ issue.title
17301
+ ),
17302
+ /* @__PURE__ */ React10.createElement(Text8, { style: { fontSize: 11, color: colors.textMuted } }, issue.status)
17303
+ )))), isBugType && /* @__PURE__ */ React10.createElement(View10, { style: styles5.section }, /* @__PURE__ */ React10.createElement(Text8, { style: shared.label }, "Which screen?"), /* @__PURE__ */ React10.createElement(
16931
17304
  TextInput4,
16932
17305
  {
16933
17306
  style: styles5.screenInput,
@@ -17003,11 +17376,24 @@ function createStyles6() {
17003
17376
  }
17004
17377
 
17005
17378
  // src/widget/screens/MessageListScreen.tsx
17006
- import React12, { useMemo as useMemo7 } from "react";
17007
- import { View as View12, Text as Text10, TouchableOpacity as TouchableOpacity9, StyleSheet as StyleSheet12, Linking as Linking3 } from "react-native";
17379
+ import React12, { useMemo as useMemo7, useState as useState9 } from "react";
17380
+ import { View as View12, Text as Text10, TouchableOpacity as TouchableOpacity9, ScrollView as ScrollView3, StyleSheet as StyleSheet12, Linking as Linking3 } from "react-native";
17008
17381
  function MessageListScreen({ nav }) {
17009
17382
  const { threads, unreadCount, refreshThreads, dashboardUrl, isLoading, widgetColorScheme } = useBugBear();
17010
17383
  const styles5 = useMemo7(() => createStyles7(), [widgetColorScheme]);
17384
+ const [activeFilter, setActiveFilter] = useState9("all");
17385
+ const filteredThreads = threads.filter((thread) => {
17386
+ if (activeFilter === "all") return true;
17387
+ if (activeFilter === "unread") return thread.unreadCount > 0;
17388
+ return thread.threadType === activeFilter;
17389
+ });
17390
+ const filterChips = [
17391
+ { key: "all", label: "All" },
17392
+ { key: "report", label: "Bug Reports" },
17393
+ { key: "direct", label: "Direct" },
17394
+ { key: "announcement", label: "Announcements" },
17395
+ { key: "unread", label: "Unread", count: threads.filter((t) => t.unreadCount > 0).length }
17396
+ ];
17011
17397
  if (isLoading) return /* @__PURE__ */ React12.createElement(MessageListScreenSkeleton, null);
17012
17398
  return /* @__PURE__ */ React12.createElement(View12, null, /* @__PURE__ */ React12.createElement(
17013
17399
  TouchableOpacity9,
@@ -17016,14 +17402,54 @@ function MessageListScreen({ nav }) {
17016
17402
  onPress: () => nav.push({ name: "COMPOSE_MESSAGE" })
17017
17403
  },
17018
17404
  /* @__PURE__ */ React12.createElement(Text10, { style: styles5.newMsgText }, "\u2709\uFE0F New Message")
17019
- ), threads.length === 0 ? /* @__PURE__ */ React12.createElement(View12, { style: shared.emptyState }, /* @__PURE__ */ React12.createElement(Text10, { style: shared.emptyEmoji }, "\u{1F4AC}"), /* @__PURE__ */ React12.createElement(Text10, { style: shared.emptyTitle }, "No messages yet"), /* @__PURE__ */ React12.createElement(Text10, { style: shared.emptySubtitle }, "Start a conversation or wait for messages from admins")) : /* @__PURE__ */ React12.createElement(View12, null, threads.map((thread) => /* @__PURE__ */ React12.createElement(
17405
+ ), /* @__PURE__ */ React12.createElement(
17406
+ ScrollView3,
17407
+ {
17408
+ horizontal: true,
17409
+ showsHorizontalScrollIndicator: false,
17410
+ style: { paddingBottom: 12 },
17411
+ contentContainerStyle: { paddingHorizontal: 16, gap: 8 }
17412
+ },
17413
+ filterChips.map((chip) => /* @__PURE__ */ React12.createElement(
17414
+ TouchableOpacity9,
17415
+ {
17416
+ key: chip.key,
17417
+ onPress: () => setActiveFilter(chip.key),
17418
+ style: {
17419
+ paddingVertical: 6,
17420
+ paddingHorizontal: 12,
17421
+ borderRadius: 16,
17422
+ borderWidth: 1,
17423
+ borderColor: activeFilter === chip.key ? colors.blue : colors.border,
17424
+ backgroundColor: activeFilter === chip.key ? withAlpha(colors.blue, 0.15) : "transparent",
17425
+ flexDirection: "row",
17426
+ alignItems: "center",
17427
+ gap: 4
17428
+ }
17429
+ },
17430
+ /* @__PURE__ */ React12.createElement(Text10, { style: {
17431
+ fontSize: 12,
17432
+ fontWeight: "500",
17433
+ color: activeFilter === chip.key ? colors.blue : colors.textSecondary
17434
+ } }, chip.label),
17435
+ chip.count !== void 0 && chip.count > 0 && /* @__PURE__ */ React12.createElement(View12, { style: {
17436
+ backgroundColor: colors.blue,
17437
+ borderRadius: 8,
17438
+ minWidth: 16,
17439
+ height: 16,
17440
+ justifyContent: "center",
17441
+ alignItems: "center",
17442
+ paddingHorizontal: 4
17443
+ } }, /* @__PURE__ */ React12.createElement(Text10, { style: { fontSize: 10, fontWeight: "bold", color: colors.onPrimary } }, chip.count))
17444
+ ))
17445
+ ), filteredThreads.length === 0 ? /* @__PURE__ */ React12.createElement(View12, { style: shared.emptyState }, /* @__PURE__ */ React12.createElement(Text10, { style: shared.emptyEmoji }, "\u{1F4AC}"), /* @__PURE__ */ React12.createElement(Text10, { style: shared.emptyTitle }, "No messages yet"), /* @__PURE__ */ React12.createElement(Text10, { style: shared.emptySubtitle }, "Start a conversation or wait for messages from admins")) : /* @__PURE__ */ React12.createElement(View12, null, filteredThreads.map((thread) => /* @__PURE__ */ React12.createElement(
17020
17446
  TouchableOpacity9,
17021
17447
  {
17022
17448
  key: thread.id,
17023
17449
  style: [styles5.threadItem, thread.unreadCount > 0 && styles5.threadItemUnread],
17024
17450
  onPress: () => nav.push({ name: "THREAD_DETAIL", thread })
17025
17451
  },
17026
- /* @__PURE__ */ React12.createElement(View12, { style: styles5.threadLeft }, /* @__PURE__ */ React12.createElement(Text10, { style: styles5.threadIcon }, getThreadTypeIcon(thread.threadType)), /* @__PURE__ */ React12.createElement(View12, { style: styles5.threadInfo }, /* @__PURE__ */ React12.createElement(View12, { style: styles5.threadTitleRow }, thread.isPinned && /* @__PURE__ */ React12.createElement(Text10, { style: styles5.pinIcon }, "\u{1F4CC}"), /* @__PURE__ */ React12.createElement(Text10, { style: styles5.threadSubject, numberOfLines: 1 }, thread.subject || "No subject")), thread.lastMessage && /* @__PURE__ */ React12.createElement(Text10, { style: styles5.threadPreview, numberOfLines: 1 }, thread.lastMessage.senderName, ": ", thread.lastMessage.content))),
17452
+ /* @__PURE__ */ React12.createElement(View12, { style: styles5.threadLeft }, /* @__PURE__ */ React12.createElement(Text10, { style: styles5.threadIcon }, getThreadTypeIcon(thread.threadType)), /* @__PURE__ */ React12.createElement(View12, { style: styles5.threadInfo }, /* @__PURE__ */ React12.createElement(View12, { style: styles5.threadTitleRow }, thread.isPinned && /* @__PURE__ */ React12.createElement(Text10, { style: styles5.pinIcon }, "\u{1F4CC}"), /* @__PURE__ */ React12.createElement(Text10, { style: styles5.threadSubject, numberOfLines: 1 }, thread.subject || "No subject")), thread.threadType === "report" && thread.reporterName && /* @__PURE__ */ React12.createElement(Text10, { style: { fontSize: 11, color: colors.textMuted } }, "Reported by: ", thread.reporterName), thread.lastMessage && /* @__PURE__ */ React12.createElement(Text10, { style: styles5.threadPreview, numberOfLines: 1 }, thread.lastMessage.senderName, ": ", thread.lastMessage.content))),
17027
17453
  /* @__PURE__ */ React12.createElement(View12, { style: styles5.threadRight }, /* @__PURE__ */ React12.createElement(Text10, { style: styles5.threadTime }, formatRelativeTime(thread.lastMessageAt)), thread.unreadCount > 0 && /* @__PURE__ */ React12.createElement(View12, { style: styles5.unreadBadge }, /* @__PURE__ */ React12.createElement(Text10, { style: styles5.unreadText }, thread.unreadCount)), thread.priority !== "normal" && /* @__PURE__ */ React12.createElement(View12, { style: [styles5.priorityDot, { backgroundColor: getPriorityColor(thread.priority) }] }))
17028
17454
  ))), dashboardUrl && /* @__PURE__ */ React12.createElement(
17029
17455
  TouchableOpacity9,
@@ -17033,7 +17459,7 @@ function MessageListScreen({ nav }) {
17033
17459
  activeOpacity: 0.7
17034
17460
  },
17035
17461
  /* @__PURE__ */ React12.createElement(Text10, { style: styles5.dashboardLinkText }, "\u{1F310}", " View on Dashboard ", "\u2192")
17036
- ), /* @__PURE__ */ React12.createElement(View12, { style: styles5.footer }, /* @__PURE__ */ React12.createElement(Text10, { style: styles5.footerText }, threads.length, " thread", threads.length !== 1 ? "s" : "", " \xB7 ", unreadCount, " unread"), /* @__PURE__ */ React12.createElement(TouchableOpacity9, { onPress: refreshThreads }, /* @__PURE__ */ React12.createElement(Text10, { style: styles5.refreshText }, "\u21BB Refresh"))));
17462
+ ), /* @__PURE__ */ React12.createElement(View12, { style: styles5.footer }, /* @__PURE__ */ React12.createElement(Text10, { style: styles5.footerText }, filteredThreads.length, " thread", filteredThreads.length !== 1 ? "s" : "", " \xB7 ", unreadCount, " unread"), /* @__PURE__ */ React12.createElement(TouchableOpacity9, { onPress: refreshThreads }, /* @__PURE__ */ React12.createElement(Text10, { style: styles5.refreshText }, "\u21BB Refresh"))));
17037
17463
  }
17038
17464
  function createStyles7() {
17039
17465
  return StyleSheet12.create({
@@ -17062,16 +17488,16 @@ function createStyles7() {
17062
17488
  }
17063
17489
 
17064
17490
  // src/widget/screens/ThreadDetailScreen.tsx
17065
- import React13, { useState as useState9, useEffect as useEffect8, useMemo as useMemo8 } from "react";
17491
+ import React13, { useState as useState10, useEffect as useEffect8, useMemo as useMemo8 } from "react";
17066
17492
  import { View as View13, Text as Text11, TouchableOpacity as TouchableOpacity10, TextInput as TextInput5, StyleSheet as StyleSheet13, Image as Image2 } from "react-native";
17067
17493
  function ThreadDetailScreen({ thread, nav }) {
17068
17494
  const { getThreadMessages, sendMessage, markAsRead, uploadImage, widgetColorScheme } = useBugBear();
17069
17495
  const styles5 = useMemo8(() => createStyles8(), [widgetColorScheme]);
17070
- const [messages, setMessages] = useState9([]);
17071
- const [loading, setLoading] = useState9(true);
17072
- const [replyText, setReplyText] = useState9("");
17073
- const [sending, setSending] = useState9(false);
17074
- const [sendError, setSendError] = useState9(false);
17496
+ const [messages, setMessages] = useState10([]);
17497
+ const [loading, setLoading] = useState10(true);
17498
+ const [replyText, setReplyText] = useState10("");
17499
+ const [sending, setSending] = useState10(false);
17500
+ const [sendError, setSendError] = useState10(false);
17075
17501
  const replyImages = useImageAttachments(uploadImage, 3, "discussion-attachments");
17076
17502
  useEffect8(() => {
17077
17503
  let cancelled = false;
@@ -17183,14 +17609,14 @@ function createStyles8() {
17183
17609
  }
17184
17610
 
17185
17611
  // src/widget/screens/ComposeMessageScreen.tsx
17186
- import React14, { useState as useState10, useMemo as useMemo9 } from "react";
17612
+ import React14, { useState as useState11, useMemo as useMemo9 } from "react";
17187
17613
  import { View as View14, Text as Text12, TextInput as TextInput6, TouchableOpacity as TouchableOpacity11, StyleSheet as StyleSheet14 } from "react-native";
17188
17614
  function ComposeMessageScreen({ nav }) {
17189
17615
  const { createThread, uploadImage, widgetColorScheme } = useBugBear();
17190
17616
  const styles5 = useMemo9(() => createStyles9(), [widgetColorScheme]);
17191
- const [subject, setSubject] = useState10("");
17192
- const [message, setMessage] = useState10("");
17193
- const [sending, setSending] = useState10(false);
17617
+ const [subject, setSubject] = useState11("");
17618
+ const [message, setMessage] = useState11("");
17619
+ const [sending, setSending] = useState11(false);
17194
17620
  const images = useImageAttachments(uploadImage, 3, "discussion-attachments");
17195
17621
  const handleSend = async () => {
17196
17622
  if (!subject.trim() || !message.trim() || sending || images.isUploading) return;
@@ -17260,19 +17686,19 @@ function createStyles9() {
17260
17686
  }
17261
17687
 
17262
17688
  // src/widget/screens/ProfileScreen.tsx
17263
- import React15, { useState as useState11, useEffect as useEffect9, useMemo as useMemo10 } from "react";
17689
+ import React15, { useState as useState12, useEffect as useEffect9, useMemo as useMemo10 } from "react";
17264
17690
  import { View as View15, Text as Text13, TouchableOpacity as TouchableOpacity12, TextInput as TextInput7, StyleSheet as StyleSheet15 } from "react-native";
17265
17691
  function ProfileScreen({ nav }) {
17266
17692
  const { testerInfo, assignments, updateTesterProfile, refreshTesterInfo, widgetColorScheme } = useBugBear();
17267
17693
  const styles5 = useMemo10(() => createStyles10(), [widgetColorScheme]);
17268
- const [editing, setEditing] = useState11(false);
17269
- const [name, setName] = useState11(testerInfo?.name || "");
17270
- const [additionalEmails, setAdditionalEmails] = useState11(testerInfo?.additionalEmails || []);
17271
- const [newEmailInput, setNewEmailInput] = useState11("");
17272
- const [platforms, setPlatforms] = useState11(testerInfo?.platforms || []);
17273
- const [saving, setSaving] = useState11(false);
17274
- const [saved, setSaved] = useState11(false);
17275
- const [showDetails, setShowDetails] = useState11(false);
17694
+ const [editing, setEditing] = useState12(false);
17695
+ const [name, setName] = useState12(testerInfo?.name || "");
17696
+ const [additionalEmails, setAdditionalEmails] = useState12(testerInfo?.additionalEmails || []);
17697
+ const [newEmailInput, setNewEmailInput] = useState12("");
17698
+ const [platforms, setPlatforms] = useState12(testerInfo?.platforms || []);
17699
+ const [saving, setSaving] = useState12(false);
17700
+ const [saved, setSaved] = useState12(false);
17701
+ const [showDetails, setShowDetails] = useState12(false);
17276
17702
  const completedCount = assignments.filter((a) => a.status === "passed" || a.status === "failed").length;
17277
17703
  useEffect9(() => {
17278
17704
  if (testerInfo) {
@@ -17391,8 +17817,8 @@ function createStyles10() {
17391
17817
  }
17392
17818
 
17393
17819
  // src/widget/screens/IssueListScreen.tsx
17394
- import React16, { useState as useState12, useEffect as useEffect10, useMemo as useMemo11 } from "react";
17395
- import { View as View16, Text as Text14, TouchableOpacity as TouchableOpacity13, StyleSheet as StyleSheet16 } from "react-native";
17820
+ import React16, { useState as useState13, useEffect as useEffect10, useMemo as useMemo11 } from "react";
17821
+ import { View as View16, Text as Text14, TextInput as TextInput8, TouchableOpacity as TouchableOpacity13, StyleSheet as StyleSheet16 } from "react-native";
17396
17822
  var CATEGORIES = ["open", "done", "reopened"];
17397
17823
  var SEVERITY_ORDER2 = { critical: 0, high: 1, medium: 2, low: 3 };
17398
17824
  function IssueListScreen({ nav, category }) {
@@ -17409,12 +17835,22 @@ function IssueListScreen({ nav, category }) {
17409
17835
  medium: colors.yellow,
17410
17836
  low: colors.textMuted
17411
17837
  }), [widgetColorScheme]);
17412
- const [activeCategory, setActiveCategory] = useState12(category);
17413
- const [issues, setIssues] = useState12([]);
17414
- const [loading, setLoading] = useState12(true);
17415
- const [counts, setCounts] = useState12(null);
17416
- const [sortMode, setSortMode] = useState12("severity");
17838
+ const [activeCategory, setActiveCategory] = useState13(category);
17839
+ const [issues, setIssues] = useState13([]);
17840
+ const [loading, setLoading] = useState13(true);
17841
+ const [counts, setCounts] = useState13(null);
17842
+ const [sortMode, setSortMode] = useState13("severity");
17843
+ const [searchQuery, setSearchQuery] = useState13("");
17844
+ const [debouncedQuery, setDebouncedQuery] = useState13("");
17417
17845
  const config = CATEGORY_CONFIG[activeCategory];
17846
+ useEffect10(() => {
17847
+ const timer = setTimeout(() => setDebouncedQuery(searchQuery), 300);
17848
+ return () => clearTimeout(timer);
17849
+ }, [searchQuery]);
17850
+ useEffect10(() => {
17851
+ setSearchQuery("");
17852
+ setDebouncedQuery("");
17853
+ }, [activeCategory]);
17418
17854
  useEffect10(() => {
17419
17855
  if (!client) return;
17420
17856
  client.getIssueCounts().then(setCounts).catch(() => {
@@ -17454,6 +17890,9 @@ function IssueListScreen({ nav, category }) {
17454
17890
  }
17455
17891
  return sorted;
17456
17892
  }, [issues, sortMode]);
17893
+ const searchFilteredIssues = debouncedQuery ? sortedIssues.filter(
17894
+ (issue) => (issue.title || "").toLowerCase().includes(debouncedQuery.toLowerCase()) || (issue.description || "").toLowerCase().includes(debouncedQuery.toLowerCase())
17895
+ ) : sortedIssues;
17457
17896
  return /* @__PURE__ */ React16.createElement(View16, null, /* @__PURE__ */ React16.createElement(View16, { style: styles5.tabBar }, CATEGORIES.map((cat) => {
17458
17897
  const catConfig = CATEGORY_CONFIG[cat];
17459
17898
  const isActive = activeCategory === cat;
@@ -17500,7 +17939,25 @@ function IssueListScreen({ nav, category }) {
17500
17939
  styles5.sortBtnText,
17501
17940
  sortMode === s2.key && styles5.sortBtnTextActive
17502
17941
  ] }, s2.label)
17503
- ))), loading ? /* @__PURE__ */ React16.createElement(IssueListScreenSkeleton, null) : sortedIssues.length === 0 ? /* @__PURE__ */ React16.createElement(View16, { style: styles5.emptyContainer }, /* @__PURE__ */ React16.createElement(Text14, { style: styles5.emptyIcon }, config.emptyIcon), /* @__PURE__ */ React16.createElement(Text14, { style: styles5.emptyText }, config.emptyText)) : sortedIssues.map((issue) => /* @__PURE__ */ React16.createElement(
17942
+ ))), /* @__PURE__ */ React16.createElement(View16, { style: { paddingHorizontal: 16, paddingBottom: 12 } }, /* @__PURE__ */ React16.createElement(
17943
+ TextInput8,
17944
+ {
17945
+ value: searchQuery,
17946
+ onChangeText: setSearchQuery,
17947
+ placeholder: "Search my reports...",
17948
+ placeholderTextColor: colors.textMuted,
17949
+ style: {
17950
+ padding: 8,
17951
+ paddingHorizontal: 12,
17952
+ borderRadius: 8,
17953
+ borderWidth: 1,
17954
+ borderColor: colors.border,
17955
+ backgroundColor: colors.card,
17956
+ color: colors.textPrimary,
17957
+ fontSize: 13
17958
+ }
17959
+ }
17960
+ )), loading ? /* @__PURE__ */ React16.createElement(IssueListScreenSkeleton, null) : searchFilteredIssues.length === 0 ? /* @__PURE__ */ React16.createElement(View16, { style: styles5.emptyContainer }, /* @__PURE__ */ React16.createElement(Text14, { style: styles5.emptyIcon }, debouncedQuery ? "\u{1F50D}" : config.emptyIcon), /* @__PURE__ */ React16.createElement(Text14, { style: styles5.emptyText }, debouncedQuery ? "No matching issues" : config.emptyText)) : searchFilteredIssues.map((issue) => /* @__PURE__ */ React16.createElement(
17504
17961
  TouchableOpacity13,
17505
17962
  {
17506
17963
  key: issue.id,
@@ -17665,11 +18122,17 @@ function createStyles11() {
17665
18122
  }
17666
18123
 
17667
18124
  // 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";
18125
+ import React17, { useMemo as useMemo12, useState as useState14, useCallback as useCallback5 } from "react";
18126
+ import { View as View17, Text as Text15, Image as Image3, StyleSheet as StyleSheet17, Linking as Linking4, TouchableOpacity as TouchableOpacity14, TextInput as TextInput9, ActivityIndicator as ActivityIndicator2 } from "react-native";
18127
+ var DONE_STATUSES = ["verified", "resolved", "closed", "reviewed"];
17670
18128
  function IssueDetailScreen({ nav, issue }) {
17671
- const { dashboardUrl, widgetColorScheme } = useBugBear();
18129
+ const { dashboardUrl, widgetColorScheme, reopenReport } = useBugBear();
17672
18130
  const styles5 = useMemo12(() => createStyles12(), [widgetColorScheme]);
18131
+ const [showReopenForm, setShowReopenForm] = useState14(false);
18132
+ const [reopenReason, setReopenReason] = useState14("");
18133
+ const [isSubmitting, setIsSubmitting] = useState14(false);
18134
+ const [reopenError, setReopenError] = useState14(null);
18135
+ const [wasReopened, setWasReopened] = useState14(false);
17673
18136
  const STATUS_LABELS = useMemo12(() => ({
17674
18137
  new: { label: "New", bg: colors.blueDark, color: colors.blueLight },
17675
18138
  triaging: { label: "Triaging", bg: colors.blueDark, color: colors.blueLight },
@@ -17692,7 +18155,68 @@ function IssueDetailScreen({ nav, issue }) {
17692
18155
  }), [widgetColorScheme]);
17693
18156
  const statusConfig = STATUS_LABELS[issue.status] || { label: issue.status, bg: colors.card, color: colors.textSecondary };
17694
18157
  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(
18158
+ const isDone = DONE_STATUSES.includes(issue.status);
18159
+ const handleReopen = useCallback5(async () => {
18160
+ if (!reopenReason.trim()) return;
18161
+ setIsSubmitting(true);
18162
+ setReopenError(null);
18163
+ const result = await reopenReport(issue.id, reopenReason.trim());
18164
+ setIsSubmitting(false);
18165
+ if (result.success) {
18166
+ setWasReopened(true);
18167
+ setShowReopenForm(false);
18168
+ } else {
18169
+ setReopenError(result.error || "Failed to reopen");
18170
+ }
18171
+ }, [reopenReason, reopenReport, issue.id]);
18172
+ 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(
18173
+ TouchableOpacity14,
18174
+ {
18175
+ style: styles5.reopenButton,
18176
+ onPress: () => setShowReopenForm(true),
18177
+ activeOpacity: 0.7
18178
+ },
18179
+ /* @__PURE__ */ React17.createElement(Text15, { style: styles5.reopenButtonText }, "\u{1F504}", " Not Fixed \u2014 Reopen Issue")
18180
+ ), showReopenForm && !wasReopened && /* @__PURE__ */ React17.createElement(View17, { style: styles5.reopenForm }, /* @__PURE__ */ React17.createElement(Text15, { style: styles5.reopenFormTitle }, "Why isn't this fixed?"), /* @__PURE__ */ React17.createElement(
18181
+ TextInput9,
18182
+ {
18183
+ value: reopenReason,
18184
+ onChangeText: setReopenReason,
18185
+ placeholder: "Describe what you're still seeing...",
18186
+ placeholderTextColor: colors.textMuted,
18187
+ style: styles5.reopenInput,
18188
+ multiline: true,
18189
+ autoFocus: true
18190
+ }
18191
+ ), reopenError && /* @__PURE__ */ React17.createElement(Text15, { style: styles5.reopenErrorText }, reopenError), /* @__PURE__ */ React17.createElement(View17, { style: styles5.reopenActions }, /* @__PURE__ */ React17.createElement(
18192
+ TouchableOpacity14,
18193
+ {
18194
+ style: [
18195
+ styles5.reopenSubmitButton,
18196
+ (!reopenReason.trim() || isSubmitting) && styles5.reopenSubmitDisabled
18197
+ ],
18198
+ onPress: handleReopen,
18199
+ disabled: isSubmitting || !reopenReason.trim(),
18200
+ activeOpacity: 0.7
18201
+ },
18202
+ isSubmitting ? /* @__PURE__ */ React17.createElement(ActivityIndicator2, { size: "small", color: "#fff" }) : /* @__PURE__ */ React17.createElement(Text15, { style: [
18203
+ styles5.reopenSubmitText,
18204
+ !reopenReason.trim() && styles5.reopenSubmitTextDisabled
18205
+ ] }, "Reopen Issue")
18206
+ ), /* @__PURE__ */ React17.createElement(
18207
+ TouchableOpacity14,
18208
+ {
18209
+ style: styles5.reopenCancelButton,
18210
+ onPress: () => {
18211
+ setShowReopenForm(false);
18212
+ setReopenReason("");
18213
+ setReopenError(null);
18214
+ },
18215
+ disabled: isSubmitting,
18216
+ activeOpacity: 0.7
18217
+ },
18218
+ /* @__PURE__ */ React17.createElement(Text15, { style: styles5.reopenCancelText }, "Cancel")
18219
+ ))), 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
18220
  TouchableOpacity14,
17697
18221
  {
17698
18222
  style: styles5.dashboardLink,
@@ -17744,6 +18268,28 @@ function createStyles12() {
17744
18268
  color: colors.textSecondary,
17745
18269
  lineHeight: 19
17746
18270
  },
18271
+ reopenedCard: {
18272
+ backgroundColor: colors.yellowDark,
18273
+ borderWidth: 1,
18274
+ borderColor: colors.yellowBorder,
18275
+ borderRadius: 8,
18276
+ padding: 12,
18277
+ marginBottom: 12
18278
+ },
18279
+ reopenedRow: {
18280
+ flexDirection: "row",
18281
+ alignItems: "center",
18282
+ gap: 8
18283
+ },
18284
+ reopenedIcon: {
18285
+ fontSize: 14
18286
+ },
18287
+ reopenedText: {
18288
+ fontSize: 13,
18289
+ fontWeight: "600",
18290
+ color: colors.yellowLight,
18291
+ flex: 1
18292
+ },
17747
18293
  verifiedCard: {
17748
18294
  backgroundColor: colors.greenDark,
17749
18295
  borderWidth: 1,
@@ -17787,7 +18333,7 @@ function createStyles12() {
17787
18333
  originalBugIcon: {
17788
18334
  fontSize: 16
17789
18335
  },
17790
- originalBugTitle: {
18336
+ originalBugTitleText: {
17791
18337
  fontSize: 13,
17792
18338
  fontWeight: "600",
17793
18339
  color: colors.yellowLight
@@ -17796,6 +18342,89 @@ function createStyles12() {
17796
18342
  fontSize: 12,
17797
18343
  color: colors.yellowSubtle
17798
18344
  },
18345
+ reopenButton: {
18346
+ borderWidth: 1,
18347
+ borderColor: colors.orange,
18348
+ borderRadius: 8,
18349
+ paddingVertical: 10,
18350
+ paddingHorizontal: 16,
18351
+ alignItems: "center",
18352
+ marginBottom: 12
18353
+ },
18354
+ reopenButtonText: {
18355
+ fontSize: 13,
18356
+ fontWeight: "600",
18357
+ color: colors.orange
18358
+ },
18359
+ reopenForm: {
18360
+ backgroundColor: colors.card,
18361
+ borderWidth: 1,
18362
+ borderColor: colors.orange,
18363
+ borderRadius: 8,
18364
+ padding: 12,
18365
+ marginBottom: 12
18366
+ },
18367
+ reopenFormTitle: {
18368
+ fontSize: 13,
18369
+ fontWeight: "600",
18370
+ color: colors.orange,
18371
+ marginBottom: 8
18372
+ },
18373
+ reopenInput: {
18374
+ backgroundColor: colors.bg,
18375
+ borderWidth: 1,
18376
+ borderColor: colors.border,
18377
+ borderRadius: 6,
18378
+ padding: 8,
18379
+ color: colors.textPrimary,
18380
+ fontSize: 13,
18381
+ lineHeight: 18,
18382
+ minHeight: 60,
18383
+ textAlignVertical: "top"
18384
+ },
18385
+ reopenErrorText: {
18386
+ fontSize: 12,
18387
+ color: colors.red,
18388
+ marginTop: 6
18389
+ },
18390
+ reopenActions: {
18391
+ flexDirection: "row",
18392
+ gap: 8,
18393
+ marginTop: 8
18394
+ },
18395
+ reopenSubmitButton: {
18396
+ flex: 1,
18397
+ backgroundColor: colors.orange,
18398
+ borderRadius: 6,
18399
+ paddingVertical: 8,
18400
+ paddingHorizontal: 12,
18401
+ alignItems: "center",
18402
+ justifyContent: "center"
18403
+ },
18404
+ reopenSubmitDisabled: {
18405
+ backgroundColor: colors.card,
18406
+ opacity: 0.7
18407
+ },
18408
+ reopenSubmitText: {
18409
+ fontSize: 13,
18410
+ fontWeight: "600",
18411
+ color: "#fff"
18412
+ },
18413
+ reopenSubmitTextDisabled: {
18414
+ color: colors.textMuted
18415
+ },
18416
+ reopenCancelButton: {
18417
+ borderWidth: 1,
18418
+ borderColor: colors.border,
18419
+ borderRadius: 6,
18420
+ paddingVertical: 8,
18421
+ paddingHorizontal: 12,
18422
+ alignItems: "center"
18423
+ },
18424
+ reopenCancelText: {
18425
+ fontSize: 13,
18426
+ color: colors.textSecondary
18427
+ },
17799
18428
  screenshotSection: {
17800
18429
  marginBottom: 12
17801
18430
  },
@@ -17847,18 +18476,20 @@ function createStyles12() {
17847
18476
  }
17848
18477
 
17849
18478
  // 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";
18479
+ import React18, { useState as useState15, useMemo as useMemo13 } from "react";
18480
+ import { View as View18, Text as Text16, TextInput as TextInput10, TouchableOpacity as TouchableOpacity15, StyleSheet as StyleSheet18, Keyboard as Keyboard2 } from "react-native";
17852
18481
  function SessionStartScreen({ nav }) {
17853
18482
  const { startSession, assignments, widgetColorScheme } = useBugBear();
17854
18483
  const styles5 = useMemo13(() => createStyles13(), [widgetColorScheme]);
17855
- const [focusArea, setFocusArea] = useState13("");
17856
- const [isStarting, setIsStarting] = useState13(false);
18484
+ const [focusArea, setFocusArea] = useState15("");
18485
+ const [isStarting, setIsStarting] = useState15(false);
18486
+ const [error, setError] = useState15(null);
17857
18487
  const trackNames = Array.from(new Set(
17858
18488
  assignments.filter((a) => a.testCase.track?.name).map((a) => a.testCase.track.name)
17859
18489
  )).slice(0, 6);
17860
18490
  const handleStart = async () => {
17861
18491
  if (isStarting) return;
18492
+ setError(null);
17862
18493
  Keyboard2.dismiss();
17863
18494
  setIsStarting(true);
17864
18495
  try {
@@ -17867,13 +18498,17 @@ function SessionStartScreen({ nav }) {
17867
18498
  });
17868
18499
  if (result.success) {
17869
18500
  nav.replace({ name: "SESSION_ACTIVE" });
18501
+ } else {
18502
+ const msg = result.error || "Failed to start session. Please try again.";
18503
+ console.warn("BugBear: Session start failed:", msg);
18504
+ setError(msg);
17870
18505
  }
17871
18506
  } finally {
17872
18507
  setIsStarting(false);
17873
18508
  }
17874
18509
  };
17875
18510
  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,
18511
+ TextInput10,
17877
18512
  {
17878
18513
  value: focusArea,
17879
18514
  onChangeText: setFocusArea,
@@ -17892,7 +18527,7 @@ function SessionStartScreen({ nav }) {
17892
18527
  activeOpacity: 0.7
17893
18528
  },
17894
18529
  /* @__PURE__ */ React18.createElement(Text16, { style: [styles5.chipText, focusArea === name && styles5.chipTextActive] }, name)
17895
- )))), /* @__PURE__ */ React18.createElement(
18530
+ )))), error && /* @__PURE__ */ React18.createElement(View18, { style: styles5.errorBox }, /* @__PURE__ */ React18.createElement(Text16, { style: styles5.errorText }, error)), /* @__PURE__ */ React18.createElement(
17896
18531
  TouchableOpacity15,
17897
18532
  {
17898
18533
  style: [styles5.startBtn, isStarting && styles5.startBtnDisabled],
@@ -17971,6 +18606,20 @@ function createStyles13() {
17971
18606
  chipTextActive: {
17972
18607
  color: colors.blueLight
17973
18608
  },
18609
+ errorBox: {
18610
+ marginBottom: 12,
18611
+ paddingVertical: 10,
18612
+ paddingHorizontal: 12,
18613
+ backgroundColor: "rgba(239, 68, 68, 0.1)",
18614
+ borderWidth: 1,
18615
+ borderColor: "rgba(239, 68, 68, 0.3)",
18616
+ borderRadius: 8
18617
+ },
18618
+ errorText: {
18619
+ fontSize: 13,
18620
+ color: "#f87171",
18621
+ lineHeight: 18
18622
+ },
17974
18623
  startBtn: {
17975
18624
  paddingVertical: 14,
17976
18625
  backgroundColor: colors.blueSurface,
@@ -17991,7 +18640,7 @@ function createStyles13() {
17991
18640
  }
17992
18641
 
17993
18642
  // src/widget/screens/SessionActiveScreen.tsx
17994
- import React19, { useState as useState14, useEffect as useEffect11, useRef as useRef5, useMemo as useMemo14 } from "react";
18643
+ import React19, { useState as useState16, useEffect as useEffect11, useRef as useRef5, useMemo as useMemo14 } from "react";
17995
18644
  import { View as View19, Text as Text17, TouchableOpacity as TouchableOpacity16, StyleSheet as StyleSheet19 } from "react-native";
17996
18645
  function SessionActiveScreen({ nav }) {
17997
18646
  const { activeSession, sessionFindings, endSession, refreshSession, widgetColorScheme } = useBugBear();
@@ -18002,8 +18651,8 @@ function SessionActiveScreen({ nav }) {
18002
18651
  suggestion: { icon: "\u{1F4A1}", label: "Suggestion", color: colors.blue },
18003
18652
  question: { icon: "\u2753", label: "Question", color: colors.violet }
18004
18653
  }), [widgetColorScheme]);
18005
- const [isEnding, setIsEnding] = useState14(false);
18006
- const [elapsed, setElapsed] = useState14(0);
18654
+ const [isEnding, setIsEnding] = useState16(false);
18655
+ const [elapsed, setElapsed] = useState16(0);
18007
18656
  const timerRef = useRef5(null);
18008
18657
  useEffect11(() => {
18009
18658
  refreshSession();
@@ -18228,8 +18877,8 @@ function createStyles14() {
18228
18877
  }
18229
18878
 
18230
18879
  // 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";
18880
+ import React20, { useState as useState17, useMemo as useMemo15 } from "react";
18881
+ import { View as View20, Text as Text18, TextInput as TextInput11, TouchableOpacity as TouchableOpacity17, StyleSheet as StyleSheet20, Keyboard as Keyboard3 } from "react-native";
18233
18882
  var FINDING_TYPES = [
18234
18883
  { value: "bug", icon: "\u{1F41B}", label: "Bug" },
18235
18884
  { value: "concern", icon: "\u26A0\uFE0F", label: "Concern" },
@@ -18246,11 +18895,11 @@ function SessionFindingScreen({ nav }) {
18246
18895
  { value: "low", label: "Low", color: colors.textMuted },
18247
18896
  { value: "observation", label: "Note", color: colors.textDim }
18248
18897
  ], [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);
18898
+ const [type, setType] = useState17("bug");
18899
+ const [severity, setSeverity] = useState17("medium");
18900
+ const [title, setTitle] = useState17("");
18901
+ const [description, setDescription] = useState17("");
18902
+ const [isSubmitting, setIsSubmitting] = useState17(false);
18254
18903
  const handleSubmit = async () => {
18255
18904
  if (!title.trim() || isSubmitting) return;
18256
18905
  Keyboard3.dismiss();
@@ -18293,7 +18942,7 @@ function SessionFindingScreen({ nav }) {
18293
18942
  },
18294
18943
  /* @__PURE__ */ React20.createElement(Text18, { style: [styles5.severityText, severity === s2.value && { color: s2.color }] }, s2.label)
18295
18944
  )))), /* @__PURE__ */ React20.createElement(View20, { style: styles5.inputSection }, /* @__PURE__ */ React20.createElement(
18296
- TextInput9,
18945
+ TextInput11,
18297
18946
  {
18298
18947
  value: title,
18299
18948
  onChangeText: setTitle,
@@ -18303,7 +18952,7 @@ function SessionFindingScreen({ nav }) {
18303
18952
  returnKeyType: "next"
18304
18953
  }
18305
18954
  )), /* @__PURE__ */ React20.createElement(View20, { style: styles5.inputSection }, /* @__PURE__ */ React20.createElement(
18306
- TextInput9,
18955
+ TextInput11,
18307
18956
  {
18308
18957
  value: description,
18309
18958
  onChangeText: setDescription,
@@ -18448,7 +19097,7 @@ function BugBearButton({
18448
19097
  }) {
18449
19098
  const { shouldShowWidget, testerInfo, isLoading, unreadCount, assignments, widgetMode, widgetColorScheme } = useBugBear();
18450
19099
  const { currentScreen, canGoBack, push, pop, replace, reset } = useNavigation();
18451
- const [modalVisible, setModalVisible] = useState16(false);
19100
+ const [modalVisible, setModalVisible] = useState18(false);
18452
19101
  const styles5 = useMemo16(() => createStyles16(), [widgetColorScheme]);
18453
19102
  const screenCaptureRef = useRef6(null);
18454
19103
  const openModal = () => {
@@ -18656,14 +19305,14 @@ function BugBearButton({
18656
19305
  style: styles5.modalOverlay
18657
19306
  },
18658
19307
  /* @__PURE__ */ React21.createElement(View21, { style: styles5.modalContainer }, /* @__PURE__ */ React21.createElement(View21, { style: styles5.header }, /* @__PURE__ */ React21.createElement(View21, { style: styles5.headerLeft }, canGoBack ? /* @__PURE__ */ React21.createElement(TouchableOpacity18, { onPress: () => nav.pop(), style: styles5.backButton }, /* @__PURE__ */ React21.createElement(Text19, { style: styles5.backText }, "\u2190 Back")) : /* @__PURE__ */ React21.createElement(View21, { style: styles5.headerTitleRow }, /* @__PURE__ */ React21.createElement(Text19, { style: styles5.headerTitle }, "BugBear"), testerInfo && /* @__PURE__ */ React21.createElement(TouchableOpacity18, { onPress: () => push({ name: "PROFILE" }) }, /* @__PURE__ */ React21.createElement(Text19, { style: styles5.headerName }, testerInfo.name, " \u270E")))), getHeaderTitle() ? /* @__PURE__ */ React21.createElement(Text19, { style: styles5.headerScreenTitle, numberOfLines: 1 }, getHeaderTitle()) : null, /* @__PURE__ */ React21.createElement(View21, { style: styles5.headerActions }, currentScreen.name !== "HOME" && /* @__PURE__ */ React21.createElement(TouchableOpacity18, { onPress: () => nav.reset(), style: styles5.homeButton }, /* @__PURE__ */ React21.createElement(Text19, { style: styles5.homeIcon }, "\u2302")), /* @__PURE__ */ React21.createElement(TouchableOpacity18, { onPress: handleClose, style: styles5.closeButton }, /* @__PURE__ */ React21.createElement(Text19, { style: styles5.closeText }, "\u2715")))), /* @__PURE__ */ React21.createElement(
18659
- ScrollView3,
19308
+ ScrollView4,
18660
19309
  {
18661
19310
  style: styles5.content,
18662
19311
  contentContainerStyle: styles5.contentContainer,
18663
19312
  keyboardShouldPersistTaps: "handled",
18664
19313
  showsVerticalScrollIndicator: false
18665
19314
  },
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()
19315
+ 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
19316
  ))
18668
19317
  )
18669
19318
  ));