@bbearai/react-native 0.3.8 → 0.3.10

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/README.md CHANGED
@@ -34,6 +34,7 @@ function App() {
34
34
  getCurrentUser: async () => ({
35
35
  id: user.id,
36
36
  email: user.email,
37
+ name: user.name, // Optional, shown on reports
37
38
  }),
38
39
  }}
39
40
  >
@@ -44,6 +45,138 @@ function App() {
44
45
  }
45
46
  ```
46
47
 
48
+ ## Configuration Options
49
+
50
+ ```tsx
51
+ <BugBearProvider
52
+ config={{
53
+ // Required
54
+ projectId: 'your-project-id',
55
+ getCurrentUser: async () => ({ id: user.id, email: user.email }),
56
+
57
+ // Optional — rich context for bug reports
58
+ getAppContext: () => ({
59
+ currentRoute: currentScreenName, // Current screen name
60
+ userRole: currentUser?.role, // 'owner', 'manager', 'guest', etc.
61
+ propertyId: selectedProperty?.id, // App-specific context
62
+ custom: { theme: 'dark' }, // Any additional data
63
+ }),
64
+
65
+ // Optional — callbacks
66
+ dashboardUrl: 'https://app.bugbear.ai', // Web dashboard link in widget
67
+ onNavigate: (route) => navigation.navigate(route), // Deep link handler for test cases
68
+ onReportSubmitted: (report) => { ... }, // After report submission
69
+ getNavigationHistory: () => [...recentScreens], // Custom nav tracking
70
+
71
+ // Optional — self-hosted
72
+ supabaseUrl: '...',
73
+ supabaseAnonKey: '...',
74
+ }}
75
+ enabled={isAuthReady} // Delay init until auth is ready (default: true)
76
+ >
77
+ ```
78
+
79
+ ### Why use `getAppContext`?
80
+
81
+ When a tester reports a bug, BugBear attaches the app context to the report. This tells the developer fixing the bug:
82
+ - **Which screen** the bug is on
83
+ - **What role** the user has (critical for role-dependent bugs)
84
+ - **App-specific state** like selected property, active filters, etc.
85
+
86
+ The bug report form also includes a **"Which screen?"** field so testers can specify the affected screen manually.
87
+
88
+ ## Automatic Context Capture
89
+
90
+ BugBearProvider automatically captures debugging context with zero configuration:
91
+
92
+ | Data | Details |
93
+ |------|---------|
94
+ | **Console logs** | Last 50 `console.log/warn/error/info` calls |
95
+ | **Network requests** | Last 20 `fetch()` calls with method, URL, status, duration |
96
+ | **Failed response bodies** | First 500 chars of 4xx/5xx response bodies |
97
+ | **Performance** | Page load time, memory usage |
98
+ | **Environment** | Language, timezone, online status |
99
+
100
+ This data is attached to every bug report and available to the MCP server's `get_report_context` tool for AI-assisted debugging.
101
+
102
+ > **Note:** For React Native, navigation history is not auto-captured (no `pushState`). Use `getNavigationHistory` or React Navigation integration below for screen tracking.
103
+
104
+ ## Navigation Tracking
105
+
106
+ ### React Navigation integration
107
+
108
+ ```tsx
109
+ import { useNavigationContainerRef } from '@react-navigation/native';
110
+
111
+ function App() {
112
+ const navigationRef = useNavigationContainerRef();
113
+ const routeHistory = useRef<string[]>([]);
114
+
115
+ return (
116
+ <BugBearProvider
117
+ config={{
118
+ projectId: 'your-project-id',
119
+ getCurrentUser: async () => ({ id: user.id, email: user.email }),
120
+ getNavigationHistory: () => routeHistory.current,
121
+ getAppContext: () => ({
122
+ currentRoute: navigationRef.getCurrentRoute()?.name || 'unknown',
123
+ userRole: currentUser?.role,
124
+ }),
125
+ }}
126
+ >
127
+ <NavigationContainer
128
+ ref={navigationRef}
129
+ onStateChange={() => {
130
+ const name = navigationRef.getCurrentRoute()?.name;
131
+ if (name) {
132
+ routeHistory.current.push(name);
133
+ // Keep last 20
134
+ if (routeHistory.current.length > 20) routeHistory.current.shift();
135
+ }
136
+ }}
137
+ >
138
+ {/* Your navigation */}
139
+ </NavigationContainer>
140
+ <BugBearButton />
141
+ </BugBearProvider>
142
+ );
143
+ }
144
+ ```
145
+
146
+ ## Widget Architecture
147
+
148
+ The widget uses a navigation stack pattern with 10 screens:
149
+
150
+ | Screen | Purpose |
151
+ |--------|---------|
152
+ | **Home** | Smart hero banner + 2x2 action grid + progress bar + web dashboard link |
153
+ | **Test Detail** | One-test-at-a-time execution with pass/fail/skip actions |
154
+ | **Test List** | All assignments grouped by folder with filter bar |
155
+ | **Test Feedback** | Star rating + quality flags after pass/fail |
156
+ | **Report** | Bug/feedback submission with type, severity, description, affected screen |
157
+ | **Report Success** | Confirmation with auto-return to home |
158
+ | **Message List** | Thread list with unread badges + compose button |
159
+ | **Thread Detail** | Chat bubbles + reply composer |
160
+ | **Compose Message** | New thread form with subject + message |
161
+ | **Profile** | Tester info, stats, and editable fields |
162
+
163
+ ## Button Configuration
164
+
165
+ The BugBear button is draggable by default with edge-snapping behavior:
166
+
167
+ ```tsx
168
+ <BugBearButton
169
+ draggable={true} // Enable/disable dragging (default: true)
170
+ position="bottom-right" // Initial position: 'bottom-right' | 'bottom-left'
171
+ initialX={100} // Custom initial X position (optional)
172
+ initialY={500} // Custom initial Y position (optional)
173
+ minY={100} // Minimum Y from top (default: 100)
174
+ maxYOffset={160} // Max Y offset from bottom (default: 160)
175
+ />
176
+ ```
177
+
178
+ The button automatically snaps to the nearest screen edge when released.
179
+
47
180
  ## Monorepo Setup
48
181
 
49
182
  When using BugBear in a monorepo (e.g., with Turborepo, Nx, or Yarn Workspaces), you may encounter React duplicate instance errors like:
@@ -105,90 +238,6 @@ If you're using Yarn workspaces, you can also configure hoisting to ensure a sin
105
238
  }
106
239
  ```
107
240
 
108
- ## Configuration Options
109
-
110
- ```tsx
111
- <BugBearProvider
112
- config={{
113
- projectId: 'your-project-id', // Required: Your BugBear project ID
114
- getCurrentUser: async () => {...}, // Required: Return current user info
115
- dashboardUrl: 'https://app.bugbear.ai', // Optional: Web dashboard link in widget
116
- getNavigationHistory: () => [...], // Optional: Custom navigation tracking
117
- onNavigate: (route) => {...}, // Optional: Deep link handler for test cases
118
- onReportSubmitted: (report) => {...}, // Optional: Callback after report submission
119
- }}
120
- >
121
- ```
122
-
123
- ## Widget Architecture
124
-
125
- The widget uses a navigation stack pattern with 10 screens:
126
-
127
- | Screen | Purpose |
128
- |--------|---------|
129
- | **Home** | Smart hero banner + 2x2 action grid + progress bar + web dashboard link |
130
- | **Test Detail** | One-test-at-a-time execution with pass/fail/skip actions |
131
- | **Test List** | All assignments grouped by folder with filter bar |
132
- | **Test Feedback** | Star rating + quality flags after pass/fail |
133
- | **Report** | Bug/feedback submission with type, severity, description |
134
- | **Report Success** | Confirmation with auto-return to home |
135
- | **Message List** | Thread list with unread badges + compose button |
136
- | **Thread Detail** | Chat bubbles + reply composer |
137
- | **Compose Message** | New thread form with subject + message |
138
- | **Profile** | Tester info, stats, and editable fields |
139
-
140
- ## Navigation Tracking
141
-
142
- BugBear can automatically track navigation history for better bug context. If you're using React Navigation:
143
-
144
- ```tsx
145
- import { useNavigationContainerRef } from '@react-navigation/native';
146
-
147
- function App() {
148
- const navigationRef = useNavigationContainerRef();
149
- const routeNameRef = useRef<string>();
150
-
151
- return (
152
- <BugBearProvider
153
- projectId="your-project-id"
154
- getCurrentUser={...}
155
- getNavigationHistory={() => routeNameRef.current ? [routeNameRef.current] : []}
156
- >
157
- <NavigationContainer
158
- ref={navigationRef}
159
- onReady={() => {
160
- routeNameRef.current = navigationRef.getCurrentRoute()?.name;
161
- }}
162
- onStateChange={() => {
163
- const currentRouteName = navigationRef.getCurrentRoute()?.name;
164
- routeNameRef.current = currentRouteName;
165
- }}
166
- >
167
- {/* Your navigation */}
168
- </NavigationContainer>
169
- <BugBearWidget />
170
- </BugBearProvider>
171
- );
172
- }
173
- ```
174
-
175
- ## Button Configuration
176
-
177
- The BugBear button is draggable by default with edge-snapping behavior:
178
-
179
- ```tsx
180
- <BugBearButton
181
- draggable={true} // Enable/disable dragging (default: true)
182
- position="bottom-right" // Initial position: 'bottom-right' | 'bottom-left'
183
- initialX={100} // Custom initial X position (optional)
184
- initialY={500} // Custom initial Y position (optional)
185
- minY={100} // Minimum Y from top (default: 100)
186
- maxYOffset={160} // Max Y offset from bottom (default: 160)
187
- />
188
- ```
189
-
190
- The button automatically snaps to the nearest screen edge when released.
191
-
192
241
  ## Troubleshooting
193
242
 
194
243
  ### Widget not showing
@@ -203,10 +252,12 @@ If you're using your own Supabase instance, provide the credentials:
203
252
 
204
253
  ```tsx
205
254
  <BugBearProvider
206
- projectId="your-project-id"
207
- supabaseUrl="https://your-instance.supabase.co"
208
- supabaseAnonKey="your-anon-key"
209
- getCurrentUser={...}
255
+ config={{
256
+ projectId: 'your-project-id',
257
+ supabaseUrl: 'https://your-instance.supabase.co',
258
+ supabaseAnonKey: 'your-anon-key',
259
+ getCurrentUser: async () => ({ id: user.id, email: user.email }),
260
+ }}
210
261
  >
211
262
  ```
212
263
 
@@ -216,15 +267,11 @@ If you're using your own Supabase instance, provide the credentials:
216
267
 
217
268
  The BugBear button renders within the normal React Native view hierarchy using absolute positioning. When your app displays modals, bottom sheets, or the keyboard, the button may be hidden behind them.
218
269
 
219
- This is a platform limitation - React Native doesn't provide a built-in way to render views above all modals without using a Modal component (which would block touch events on the underlying app).
220
-
221
270
  **Workarounds:**
222
271
  - Accept that the button won't be accessible when modals are open
223
272
  - Consider dismissing the modal to access BugBear
224
273
  - For testing flows that involve modals, provide an alternative way to trigger the BugBear widget
225
274
 
226
- We're investigating solutions using `react-native-portal` or similar libraries for future releases.
227
-
228
275
  ## License
229
276
 
230
277
  MIT
package/dist/index.js CHANGED
@@ -11982,19 +11982,40 @@ var BugBearClient = class {
11982
11982
  return { success: false, error: message };
11983
11983
  }
11984
11984
  }
11985
+ /**
11986
+ * Pass a test assignment — convenience wrapper around updateAssignmentStatus
11987
+ */
11988
+ async passAssignment(assignmentId) {
11989
+ return this.updateAssignmentStatus(assignmentId, "passed");
11990
+ }
11991
+ /**
11992
+ * Fail a test assignment — convenience wrapper around updateAssignmentStatus
11993
+ */
11994
+ async failAssignment(assignmentId) {
11995
+ return this.updateAssignmentStatus(assignmentId, "failed");
11996
+ }
11985
11997
  /**
11986
11998
  * Skip a test assignment with a required reason
11987
11999
  * Marks the assignment as 'skipped' and records why it was skipped
11988
12000
  */
11989
12001
  async skipAssignment(assignmentId, reason, notes) {
12002
+ let actualReason;
12003
+ let actualNotes;
12004
+ if (typeof reason === "object") {
12005
+ actualReason = reason.reason;
12006
+ actualNotes = reason.notes;
12007
+ } else {
12008
+ actualReason = reason;
12009
+ actualNotes = notes;
12010
+ }
11990
12011
  try {
11991
12012
  const updateData = {
11992
12013
  status: "skipped",
11993
- skip_reason: reason,
12014
+ skip_reason: actualReason,
11994
12015
  completed_at: (/* @__PURE__ */ new Date()).toISOString()
11995
12016
  };
11996
- if (notes) {
11997
- updateData.notes = notes;
12017
+ if (actualNotes) {
12018
+ updateData.notes = actualNotes;
11998
12019
  }
11999
12020
  const { error } = await this.supabase.from("test_assignments").update(updateData).eq("id", assignmentId);
12000
12021
  if (error) {
@@ -12634,6 +12655,7 @@ var BugBearClient = class {
12634
12655
  return false;
12635
12656
  }
12636
12657
  await this.supabase.from("discussion_threads").update({ last_message_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", threadId);
12658
+ await this.markThreadAsRead(threadId);
12637
12659
  return true;
12638
12660
  } catch (err) {
12639
12661
  console.error("BugBear: Error sending message", err);
@@ -13478,7 +13500,7 @@ function HomeScreen({ nav }) {
13478
13500
  import_react_native3.TouchableOpacity,
13479
13501
  {
13480
13502
  style: styles.actionCard,
13481
- onPress: () => nav.push({ name: "TEST_DETAIL" }),
13503
+ onPress: () => nav.push({ name: "TEST_LIST" }),
13482
13504
  activeOpacity: 0.7
13483
13505
  },
13484
13506
  /* @__PURE__ */ import_react3.default.createElement(import_react_native3.Text, { style: styles.actionIcon }, "\u2705"),
@@ -13746,12 +13768,14 @@ function TestDetailScreen({ testId, nav }) {
13746
13768
  const currentIndex = displayedAssignment ? assignments.indexOf(displayedAssignment) : -1;
13747
13769
  const handlePass = (0, import_react4.useCallback)(async () => {
13748
13770
  if (!client || !displayedAssignment) return;
13771
+ import_react_native4.Keyboard.dismiss();
13749
13772
  await client.passAssignment(displayedAssignment.id);
13750
13773
  await refreshAssignments();
13751
13774
  nav.replace({ name: "TEST_FEEDBACK", status: "passed", assignmentId: displayedAssignment.id });
13752
13775
  }, [client, displayedAssignment, refreshAssignments, nav]);
13753
13776
  const handleFail = (0, import_react4.useCallback)(async () => {
13754
13777
  if (!client || !displayedAssignment) return;
13778
+ import_react_native4.Keyboard.dismiss();
13755
13779
  await client.failAssignment(displayedAssignment.id);
13756
13780
  await refreshAssignments();
13757
13781
  nav.replace({
@@ -13765,6 +13789,7 @@ function TestDetailScreen({ testId, nav }) {
13765
13789
  }, [client, displayedAssignment, refreshAssignments, nav]);
13766
13790
  const handleSkip = (0, import_react4.useCallback)(async () => {
13767
13791
  if (!client || !displayedAssignment || !selectedSkipReason) return;
13792
+ import_react_native4.Keyboard.dismiss();
13768
13793
  setSkipping(true);
13769
13794
  await client.skipAssignment(displayedAssignment.id, {
13770
13795
  reason: selectedSkipReason,
@@ -13775,11 +13800,14 @@ function TestDetailScreen({ testId, nav }) {
13775
13800
  setSelectedSkipReason(null);
13776
13801
  setSkipNotes("");
13777
13802
  setSkipping(false);
13778
- const remaining = assignments.filter(
13803
+ const currentIdx = assignments.indexOf(displayedAssignment);
13804
+ const pending = assignments.filter(
13779
13805
  (a) => (a.status === "pending" || a.status === "in_progress") && a.id !== displayedAssignment.id
13780
13806
  );
13781
- if (remaining.length > 0) {
13782
- nav.replace({ name: "TEST_DETAIL", testId: remaining[0].id });
13807
+ if (pending.length > 0) {
13808
+ const nextAfterCurrent = pending.find((a) => assignments.indexOf(a) > currentIdx);
13809
+ const nextTest = nextAfterCurrent || pending[0];
13810
+ nav.replace({ name: "TEST_DETAIL", testId: nextTest.id });
13783
13811
  } else {
13784
13812
  nav.reset();
13785
13813
  }
@@ -13833,7 +13861,9 @@ function TestDetailScreen({ testId, nav }) {
13833
13861
  {
13834
13862
  style: styles2.navigateButton,
13835
13863
  onPress: () => {
13864
+ import_react_native4.Keyboard.dismiss();
13836
13865
  onNavigate(testCase.targetRoute);
13866
+ nav.closeWidget?.();
13837
13867
  }
13838
13868
  },
13839
13869
  /* @__PURE__ */ import_react4.default.createElement(import_react_native4.Text, { style: styles2.navigateText }, "\u{1F9ED} Go to test location")
@@ -13975,6 +14005,9 @@ function TestListScreen({ nav }) {
13975
14005
  const { assignments, currentAssignment, refreshAssignments } = useBugBear();
13976
14006
  const [filter, setFilter] = (0, import_react5.useState)("all");
13977
14007
  const [collapsedFolders, setCollapsedFolders] = (0, import_react5.useState)(/* @__PURE__ */ new Set());
14008
+ (0, import_react5.useEffect)(() => {
14009
+ refreshAssignments();
14010
+ }, []);
13978
14011
  const groupedAssignments = (0, import_react5.useMemo)(() => {
13979
14012
  const groups = /* @__PURE__ */ new Map();
13980
14013
  for (const assignment of assignments) {
@@ -14307,9 +14340,12 @@ function TestFeedbackScreen({ status, assignmentId, nav }) {
14307
14340
  if (status === "failed") {
14308
14341
  nav.replace({ name: "REPORT", prefill: { type: "test_fail", assignmentId, testCaseId: assignment?.testCase.id } });
14309
14342
  } else {
14310
- const remaining = assignments.filter((a) => (a.status === "pending" || a.status === "in_progress") && a.id !== assignmentId);
14311
- if (remaining.length > 0) {
14312
- nav.replace({ name: "TEST_DETAIL", testId: remaining[0].id });
14343
+ const currentIdx = assignments.findIndex((a) => a.id === assignmentId);
14344
+ const pending = assignments.filter((a) => (a.status === "pending" || a.status === "in_progress") && a.id !== assignmentId);
14345
+ if (pending.length > 0) {
14346
+ const nextAfterCurrent = pending.find((a) => assignments.indexOf(a) > currentIdx);
14347
+ const nextTest = nextAfterCurrent || pending[0];
14348
+ nav.replace({ name: "TEST_DETAIL", testId: nextTest.id });
14313
14349
  } else {
14314
14350
  nav.reset();
14315
14351
  }
@@ -14319,9 +14355,12 @@ function TestFeedbackScreen({ status, assignmentId, nav }) {
14319
14355
  if (status === "failed") {
14320
14356
  nav.replace({ name: "REPORT", prefill: { type: "test_fail", assignmentId, testCaseId: assignment?.testCase.id } });
14321
14357
  } else {
14322
- const remaining = assignments.filter((a) => (a.status === "pending" || a.status === "in_progress") && a.id !== assignmentId);
14323
- if (remaining.length > 0) {
14324
- nav.replace({ name: "TEST_DETAIL", testId: remaining[0].id });
14358
+ const currentIdx = assignments.findIndex((a) => a.id === assignmentId);
14359
+ const pending = assignments.filter((a) => (a.status === "pending" || a.status === "in_progress") && a.id !== assignmentId);
14360
+ if (pending.length > 0) {
14361
+ const nextAfterCurrent = pending.find((a) => assignments.indexOf(a) > currentIdx);
14362
+ const nextTest = nextAfterCurrent || pending[0];
14363
+ nav.replace({ name: "TEST_DETAIL", testId: nextTest.id });
14325
14364
  } else {
14326
14365
  nav.reset();
14327
14366
  }
@@ -14974,10 +15013,29 @@ function BugBearButton({
14974
15013
  }
14975
15014
  };
14976
15015
  const handleClose = () => {
15016
+ import_react_native15.Keyboard.dismiss();
14977
15017
  setModalVisible(false);
14978
- reset();
14979
15018
  };
14980
- const nav = { push, pop, replace, reset, canGoBack };
15019
+ const nav = {
15020
+ push: (screen) => {
15021
+ import_react_native15.Keyboard.dismiss();
15022
+ push(screen);
15023
+ },
15024
+ pop: () => {
15025
+ import_react_native15.Keyboard.dismiss();
15026
+ pop();
15027
+ },
15028
+ replace: (screen) => {
15029
+ import_react_native15.Keyboard.dismiss();
15030
+ replace(screen);
15031
+ },
15032
+ reset: () => {
15033
+ import_react_native15.Keyboard.dismiss();
15034
+ reset();
15035
+ },
15036
+ canGoBack,
15037
+ closeWidget: handleClose
15038
+ };
14981
15039
  const renderScreen = () => {
14982
15040
  switch (currentScreen.name) {
14983
15041
  case "HOME":
@@ -15034,7 +15092,7 @@ function BugBearButton({
15034
15092
  behavior: import_react_native15.Platform.OS === "ios" ? "padding" : "height",
15035
15093
  style: styles13.modalOverlay
15036
15094
  },
15037
- /* @__PURE__ */ import_react16.default.createElement(import_react_native15.View, { style: styles13.modalContainer }, /* @__PURE__ */ import_react16.default.createElement(import_react_native15.View, { style: styles13.header }, /* @__PURE__ */ import_react16.default.createElement(import_react_native15.View, { style: styles13.headerLeft }, canGoBack ? /* @__PURE__ */ import_react16.default.createElement(import_react_native15.TouchableOpacity, { onPress: pop, style: styles13.backButton }, /* @__PURE__ */ import_react16.default.createElement(import_react_native15.Text, { style: styles13.backText }, "\u2190 Back")) : /* @__PURE__ */ import_react16.default.createElement(import_react_native15.View, { style: styles13.headerTitleRow }, /* @__PURE__ */ import_react16.default.createElement(import_react_native15.Text, { style: styles13.headerTitle }, "BugBear"), testerInfo && /* @__PURE__ */ import_react16.default.createElement(import_react_native15.TouchableOpacity, { onPress: () => push({ name: "PROFILE" }) }, /* @__PURE__ */ import_react16.default.createElement(import_react_native15.Text, { style: styles13.headerName }, testerInfo.name, " \u270E")))), getHeaderTitle() ? /* @__PURE__ */ import_react16.default.createElement(import_react_native15.Text, { style: styles13.headerScreenTitle, numberOfLines: 1 }, getHeaderTitle()) : null, /* @__PURE__ */ import_react16.default.createElement(import_react_native15.TouchableOpacity, { onPress: handleClose, style: styles13.closeButton }, /* @__PURE__ */ import_react16.default.createElement(import_react_native15.Text, { style: styles13.closeText }, "\u2715"))), /* @__PURE__ */ import_react16.default.createElement(
15095
+ /* @__PURE__ */ import_react16.default.createElement(import_react_native15.View, { style: styles13.modalContainer }, /* @__PURE__ */ import_react16.default.createElement(import_react_native15.View, { style: styles13.header }, /* @__PURE__ */ import_react16.default.createElement(import_react_native15.View, { style: styles13.headerLeft }, canGoBack ? /* @__PURE__ */ import_react16.default.createElement(import_react_native15.View, { style: styles13.headerNavRow }, /* @__PURE__ */ import_react16.default.createElement(import_react_native15.TouchableOpacity, { onPress: () => nav.pop(), style: styles13.backButton }, /* @__PURE__ */ import_react16.default.createElement(import_react_native15.Text, { style: styles13.backText }, "\u2190 Back")), /* @__PURE__ */ import_react16.default.createElement(import_react_native15.TouchableOpacity, { onPress: () => nav.reset(), style: styles13.homeButton }, /* @__PURE__ */ import_react16.default.createElement(import_react_native15.Text, { style: styles13.homeText }, "\u{1F3E0}"))) : /* @__PURE__ */ import_react16.default.createElement(import_react_native15.View, { style: styles13.headerTitleRow }, /* @__PURE__ */ import_react16.default.createElement(import_react_native15.Text, { style: styles13.headerTitle }, "BugBear"), testerInfo && /* @__PURE__ */ import_react16.default.createElement(import_react_native15.TouchableOpacity, { onPress: () => push({ name: "PROFILE" }) }, /* @__PURE__ */ import_react16.default.createElement(import_react_native15.Text, { style: styles13.headerName }, testerInfo.name, " \u270E")))), getHeaderTitle() ? /* @__PURE__ */ import_react16.default.createElement(import_react_native15.Text, { style: styles13.headerScreenTitle, numberOfLines: 1 }, getHeaderTitle()) : null, /* @__PURE__ */ import_react16.default.createElement(import_react_native15.TouchableOpacity, { onPress: handleClose, style: styles13.closeButton }, /* @__PURE__ */ import_react16.default.createElement(import_react_native15.Text, { style: styles13.closeText }, "\u2715"))), /* @__PURE__ */ import_react16.default.createElement(
15038
15096
  import_react_native15.ScrollView,
15039
15097
  {
15040
15098
  style: styles13.content,
@@ -15135,15 +15193,27 @@ var styles13 = import_react_native15.StyleSheet.create({
15135
15193
  flex: 1,
15136
15194
  textAlign: "center"
15137
15195
  },
15196
+ headerNavRow: {
15197
+ flexDirection: "row",
15198
+ alignItems: "center",
15199
+ gap: 8
15200
+ },
15138
15201
  backButton: {
15139
15202
  paddingVertical: 2,
15140
- paddingRight: 12
15203
+ paddingRight: 4
15141
15204
  },
15142
15205
  backText: {
15143
15206
  fontSize: 15,
15144
15207
  color: colors.blue,
15145
15208
  fontWeight: "500"
15146
15209
  },
15210
+ homeButton: {
15211
+ paddingVertical: 2,
15212
+ paddingHorizontal: 6
15213
+ },
15214
+ homeText: {
15215
+ fontSize: 16
15216
+ },
15147
15217
  closeButton: {
15148
15218
  width: 32,
15149
15219
  height: 32,
package/dist/index.mjs CHANGED
@@ -11951,19 +11951,40 @@ var BugBearClient = class {
11951
11951
  return { success: false, error: message };
11952
11952
  }
11953
11953
  }
11954
+ /**
11955
+ * Pass a test assignment — convenience wrapper around updateAssignmentStatus
11956
+ */
11957
+ async passAssignment(assignmentId) {
11958
+ return this.updateAssignmentStatus(assignmentId, "passed");
11959
+ }
11960
+ /**
11961
+ * Fail a test assignment — convenience wrapper around updateAssignmentStatus
11962
+ */
11963
+ async failAssignment(assignmentId) {
11964
+ return this.updateAssignmentStatus(assignmentId, "failed");
11965
+ }
11954
11966
  /**
11955
11967
  * Skip a test assignment with a required reason
11956
11968
  * Marks the assignment as 'skipped' and records why it was skipped
11957
11969
  */
11958
11970
  async skipAssignment(assignmentId, reason, notes) {
11971
+ let actualReason;
11972
+ let actualNotes;
11973
+ if (typeof reason === "object") {
11974
+ actualReason = reason.reason;
11975
+ actualNotes = reason.notes;
11976
+ } else {
11977
+ actualReason = reason;
11978
+ actualNotes = notes;
11979
+ }
11959
11980
  try {
11960
11981
  const updateData = {
11961
11982
  status: "skipped",
11962
- skip_reason: reason,
11983
+ skip_reason: actualReason,
11963
11984
  completed_at: (/* @__PURE__ */ new Date()).toISOString()
11964
11985
  };
11965
- if (notes) {
11966
- updateData.notes = notes;
11986
+ if (actualNotes) {
11987
+ updateData.notes = actualNotes;
11967
11988
  }
11968
11989
  const { error } = await this.supabase.from("test_assignments").update(updateData).eq("id", assignmentId);
11969
11990
  if (error) {
@@ -12603,6 +12624,7 @@ var BugBearClient = class {
12603
12624
  return false;
12604
12625
  }
12605
12626
  await this.supabase.from("discussion_threads").update({ last_message_at: (/* @__PURE__ */ new Date()).toISOString() }).eq("id", threadId);
12627
+ await this.markThreadAsRead(threadId);
12606
12628
  return true;
12607
12629
  } catch (err) {
12608
12630
  console.error("BugBear: Error sending message", err);
@@ -13204,7 +13226,8 @@ import {
13204
13226
  Platform as Platform4,
13205
13227
  PanResponder,
13206
13228
  Animated,
13207
- ActivityIndicator as ActivityIndicator2
13229
+ ActivityIndicator as ActivityIndicator2,
13230
+ Keyboard as Keyboard2
13208
13231
  } from "react-native";
13209
13232
 
13210
13233
  // src/widget/logo.ts
@@ -13461,7 +13484,7 @@ function HomeScreen({ nav }) {
13461
13484
  TouchableOpacity,
13462
13485
  {
13463
13486
  style: styles.actionCard,
13464
- onPress: () => nav.push({ name: "TEST_DETAIL" }),
13487
+ onPress: () => nav.push({ name: "TEST_LIST" }),
13465
13488
  activeOpacity: 0.7
13466
13489
  },
13467
13490
  /* @__PURE__ */ React2.createElement(Text, { style: styles.actionIcon }, "\u2705"),
@@ -13694,7 +13717,7 @@ var styles = StyleSheet2.create({
13694
13717
 
13695
13718
  // src/widget/screens/TestDetailScreen.tsx
13696
13719
  import React3, { useState as useState2, useEffect as useEffect3, useCallback as useCallback2 } from "react";
13697
- import { View as View2, Text as Text2, TouchableOpacity as TouchableOpacity2, StyleSheet as StyleSheet3, Modal, TextInput } from "react-native";
13720
+ import { View as View2, Text as Text2, TouchableOpacity as TouchableOpacity2, StyleSheet as StyleSheet3, Modal, TextInput, Keyboard } from "react-native";
13698
13721
  function TestDetailScreen({ testId, nav }) {
13699
13722
  const { client, assignments, currentAssignment, refreshAssignments, getDeviceInfo, onNavigate } = useBugBear();
13700
13723
  const displayedAssignment = testId ? assignments.find((a) => a.id === testId) || currentAssignment : currentAssignment;
@@ -13729,12 +13752,14 @@ function TestDetailScreen({ testId, nav }) {
13729
13752
  const currentIndex = displayedAssignment ? assignments.indexOf(displayedAssignment) : -1;
13730
13753
  const handlePass = useCallback2(async () => {
13731
13754
  if (!client || !displayedAssignment) return;
13755
+ Keyboard.dismiss();
13732
13756
  await client.passAssignment(displayedAssignment.id);
13733
13757
  await refreshAssignments();
13734
13758
  nav.replace({ name: "TEST_FEEDBACK", status: "passed", assignmentId: displayedAssignment.id });
13735
13759
  }, [client, displayedAssignment, refreshAssignments, nav]);
13736
13760
  const handleFail = useCallback2(async () => {
13737
13761
  if (!client || !displayedAssignment) return;
13762
+ Keyboard.dismiss();
13738
13763
  await client.failAssignment(displayedAssignment.id);
13739
13764
  await refreshAssignments();
13740
13765
  nav.replace({
@@ -13748,6 +13773,7 @@ function TestDetailScreen({ testId, nav }) {
13748
13773
  }, [client, displayedAssignment, refreshAssignments, nav]);
13749
13774
  const handleSkip = useCallback2(async () => {
13750
13775
  if (!client || !displayedAssignment || !selectedSkipReason) return;
13776
+ Keyboard.dismiss();
13751
13777
  setSkipping(true);
13752
13778
  await client.skipAssignment(displayedAssignment.id, {
13753
13779
  reason: selectedSkipReason,
@@ -13758,11 +13784,14 @@ function TestDetailScreen({ testId, nav }) {
13758
13784
  setSelectedSkipReason(null);
13759
13785
  setSkipNotes("");
13760
13786
  setSkipping(false);
13761
- const remaining = assignments.filter(
13787
+ const currentIdx = assignments.indexOf(displayedAssignment);
13788
+ const pending = assignments.filter(
13762
13789
  (a) => (a.status === "pending" || a.status === "in_progress") && a.id !== displayedAssignment.id
13763
13790
  );
13764
- if (remaining.length > 0) {
13765
- nav.replace({ name: "TEST_DETAIL", testId: remaining[0].id });
13791
+ if (pending.length > 0) {
13792
+ const nextAfterCurrent = pending.find((a) => assignments.indexOf(a) > currentIdx);
13793
+ const nextTest = nextAfterCurrent || pending[0];
13794
+ nav.replace({ name: "TEST_DETAIL", testId: nextTest.id });
13766
13795
  } else {
13767
13796
  nav.reset();
13768
13797
  }
@@ -13816,7 +13845,9 @@ function TestDetailScreen({ testId, nav }) {
13816
13845
  {
13817
13846
  style: styles2.navigateButton,
13818
13847
  onPress: () => {
13848
+ Keyboard.dismiss();
13819
13849
  onNavigate(testCase.targetRoute);
13850
+ nav.closeWidget?.();
13820
13851
  }
13821
13852
  },
13822
13853
  /* @__PURE__ */ React3.createElement(Text2, { style: styles2.navigateText }, "\u{1F9ED} Go to test location")
@@ -13952,12 +13983,15 @@ var styles2 = StyleSheet3.create({
13952
13983
  });
13953
13984
 
13954
13985
  // src/widget/screens/TestListScreen.tsx
13955
- import React4, { useState as useState3, useMemo as useMemo2, useCallback as useCallback3 } from "react";
13986
+ import React4, { useState as useState3, useMemo as useMemo2, useCallback as useCallback3, useEffect as useEffect4 } from "react";
13956
13987
  import { View as View3, Text as Text3, TouchableOpacity as TouchableOpacity3, StyleSheet as StyleSheet4 } from "react-native";
13957
13988
  function TestListScreen({ nav }) {
13958
13989
  const { assignments, currentAssignment, refreshAssignments } = useBugBear();
13959
13990
  const [filter, setFilter] = useState3("all");
13960
13991
  const [collapsedFolders, setCollapsedFolders] = useState3(/* @__PURE__ */ new Set());
13992
+ useEffect4(() => {
13993
+ refreshAssignments();
13994
+ }, []);
13961
13995
  const groupedAssignments = useMemo2(() => {
13962
13996
  const groups = /* @__PURE__ */ new Map();
13963
13997
  for (const assignment of assignments) {
@@ -14290,9 +14324,12 @@ function TestFeedbackScreen({ status, assignmentId, nav }) {
14290
14324
  if (status === "failed") {
14291
14325
  nav.replace({ name: "REPORT", prefill: { type: "test_fail", assignmentId, testCaseId: assignment?.testCase.id } });
14292
14326
  } else {
14293
- const remaining = assignments.filter((a) => (a.status === "pending" || a.status === "in_progress") && a.id !== assignmentId);
14294
- if (remaining.length > 0) {
14295
- nav.replace({ name: "TEST_DETAIL", testId: remaining[0].id });
14327
+ const currentIdx = assignments.findIndex((a) => a.id === assignmentId);
14328
+ const pending = assignments.filter((a) => (a.status === "pending" || a.status === "in_progress") && a.id !== assignmentId);
14329
+ if (pending.length > 0) {
14330
+ const nextAfterCurrent = pending.find((a) => assignments.indexOf(a) > currentIdx);
14331
+ const nextTest = nextAfterCurrent || pending[0];
14332
+ nav.replace({ name: "TEST_DETAIL", testId: nextTest.id });
14296
14333
  } else {
14297
14334
  nav.reset();
14298
14335
  }
@@ -14302,9 +14339,12 @@ function TestFeedbackScreen({ status, assignmentId, nav }) {
14302
14339
  if (status === "failed") {
14303
14340
  nav.replace({ name: "REPORT", prefill: { type: "test_fail", assignmentId, testCaseId: assignment?.testCase.id } });
14304
14341
  } else {
14305
- const remaining = assignments.filter((a) => (a.status === "pending" || a.status === "in_progress") && a.id !== assignmentId);
14306
- if (remaining.length > 0) {
14307
- nav.replace({ name: "TEST_DETAIL", testId: remaining[0].id });
14342
+ const currentIdx = assignments.findIndex((a) => a.id === assignmentId);
14343
+ const pending = assignments.filter((a) => (a.status === "pending" || a.status === "in_progress") && a.id !== assignmentId);
14344
+ if (pending.length > 0) {
14345
+ const nextAfterCurrent = pending.find((a) => assignments.indexOf(a) > currentIdx);
14346
+ const nextTest = nextAfterCurrent || pending[0];
14347
+ nav.replace({ name: "TEST_DETAIL", testId: nextTest.id });
14308
14348
  } else {
14309
14349
  nav.reset();
14310
14350
  }
@@ -14490,10 +14530,10 @@ var styles7 = StyleSheet8.create({
14490
14530
  });
14491
14531
 
14492
14532
  // src/widget/screens/ReportSuccessScreen.tsx
14493
- import React9, { useEffect as useEffect4 } from "react";
14533
+ import React9, { useEffect as useEffect5 } from "react";
14494
14534
  import { View as View8, Text as Text8, StyleSheet as StyleSheet9 } from "react-native";
14495
14535
  function ReportSuccessScreen({ nav }) {
14496
- useEffect4(() => {
14536
+ useEffect5(() => {
14497
14537
  const timer = setTimeout(() => nav.reset(), 2e3);
14498
14538
  return () => clearTimeout(timer);
14499
14539
  }, [nav]);
@@ -14552,7 +14592,7 @@ var styles9 = StyleSheet10.create({
14552
14592
  });
14553
14593
 
14554
14594
  // src/widget/screens/ThreadDetailScreen.tsx
14555
- import React11, { useState as useState7, useEffect as useEffect5 } from "react";
14595
+ import React11, { useState as useState7, useEffect as useEffect6 } from "react";
14556
14596
  import { View as View10, Text as Text10, TouchableOpacity as TouchableOpacity9, TextInput as TextInput4, StyleSheet as StyleSheet11, Image as Image2 } from "react-native";
14557
14597
  function ThreadDetailScreen({ thread, nav }) {
14558
14598
  const { getThreadMessages, sendMessage, markAsRead, uploadImage } = useBugBear();
@@ -14562,7 +14602,7 @@ function ThreadDetailScreen({ thread, nav }) {
14562
14602
  const [sending, setSending] = useState7(false);
14563
14603
  const [sendError, setSendError] = useState7(false);
14564
14604
  const replyImages = useImageAttachments(uploadImage, 3, "discussion-attachments");
14565
- useEffect5(() => {
14605
+ useEffect6(() => {
14566
14606
  (async () => {
14567
14607
  setLoading(true);
14568
14608
  const msgs = await getThreadMessages(thread.id);
@@ -14731,7 +14771,7 @@ var styles11 = StyleSheet12.create({
14731
14771
  });
14732
14772
 
14733
14773
  // src/widget/screens/ProfileScreen.tsx
14734
- import React13, { useState as useState9, useEffect as useEffect6 } from "react";
14774
+ import React13, { useState as useState9, useEffect as useEffect7 } from "react";
14735
14775
  import { View as View12, Text as Text12, TouchableOpacity as TouchableOpacity11, TextInput as TextInput6, StyleSheet as StyleSheet13 } from "react-native";
14736
14776
  function ProfileScreen({ nav }) {
14737
14777
  const { testerInfo, assignments, updateTesterProfile, refreshTesterInfo } = useBugBear();
@@ -14744,7 +14784,7 @@ function ProfileScreen({ nav }) {
14744
14784
  const [saved, setSaved] = useState9(false);
14745
14785
  const [showDetails, setShowDetails] = useState9(false);
14746
14786
  const completedCount = assignments.filter((a) => a.status === "passed" || a.status === "failed").length;
14747
- useEffect6(() => {
14787
+ useEffect7(() => {
14748
14788
  if (testerInfo) {
14749
14789
  setName(testerInfo.name);
14750
14790
  setAdditionalEmails(testerInfo.additionalEmails || []);
@@ -14957,10 +14997,29 @@ function BugBearButton({
14957
14997
  }
14958
14998
  };
14959
14999
  const handleClose = () => {
15000
+ Keyboard2.dismiss();
14960
15001
  setModalVisible(false);
14961
- reset();
14962
15002
  };
14963
- const nav = { push, pop, replace, reset, canGoBack };
15003
+ const nav = {
15004
+ push: (screen) => {
15005
+ Keyboard2.dismiss();
15006
+ push(screen);
15007
+ },
15008
+ pop: () => {
15009
+ Keyboard2.dismiss();
15010
+ pop();
15011
+ },
15012
+ replace: (screen) => {
15013
+ Keyboard2.dismiss();
15014
+ replace(screen);
15015
+ },
15016
+ reset: () => {
15017
+ Keyboard2.dismiss();
15018
+ reset();
15019
+ },
15020
+ canGoBack,
15021
+ closeWidget: handleClose
15022
+ };
14964
15023
  const renderScreen = () => {
14965
15024
  switch (currentScreen.name) {
14966
15025
  case "HOME":
@@ -15017,7 +15076,7 @@ function BugBearButton({
15017
15076
  behavior: Platform4.OS === "ios" ? "padding" : "height",
15018
15077
  style: styles13.modalOverlay
15019
15078
  },
15020
- /* @__PURE__ */ React14.createElement(View13, { style: styles13.modalContainer }, /* @__PURE__ */ React14.createElement(View13, { style: styles13.header }, /* @__PURE__ */ React14.createElement(View13, { style: styles13.headerLeft }, canGoBack ? /* @__PURE__ */ React14.createElement(TouchableOpacity12, { onPress: pop, style: styles13.backButton }, /* @__PURE__ */ React14.createElement(Text13, { style: styles13.backText }, "\u2190 Back")) : /* @__PURE__ */ React14.createElement(View13, { style: styles13.headerTitleRow }, /* @__PURE__ */ React14.createElement(Text13, { style: styles13.headerTitle }, "BugBear"), testerInfo && /* @__PURE__ */ React14.createElement(TouchableOpacity12, { onPress: () => push({ name: "PROFILE" }) }, /* @__PURE__ */ React14.createElement(Text13, { style: styles13.headerName }, testerInfo.name, " \u270E")))), getHeaderTitle() ? /* @__PURE__ */ React14.createElement(Text13, { style: styles13.headerScreenTitle, numberOfLines: 1 }, getHeaderTitle()) : null, /* @__PURE__ */ React14.createElement(TouchableOpacity12, { onPress: handleClose, style: styles13.closeButton }, /* @__PURE__ */ React14.createElement(Text13, { style: styles13.closeText }, "\u2715"))), /* @__PURE__ */ React14.createElement(
15079
+ /* @__PURE__ */ React14.createElement(View13, { style: styles13.modalContainer }, /* @__PURE__ */ React14.createElement(View13, { style: styles13.header }, /* @__PURE__ */ React14.createElement(View13, { style: styles13.headerLeft }, canGoBack ? /* @__PURE__ */ React14.createElement(View13, { style: styles13.headerNavRow }, /* @__PURE__ */ React14.createElement(TouchableOpacity12, { onPress: () => nav.pop(), style: styles13.backButton }, /* @__PURE__ */ React14.createElement(Text13, { style: styles13.backText }, "\u2190 Back")), /* @__PURE__ */ React14.createElement(TouchableOpacity12, { onPress: () => nav.reset(), style: styles13.homeButton }, /* @__PURE__ */ React14.createElement(Text13, { style: styles13.homeText }, "\u{1F3E0}"))) : /* @__PURE__ */ React14.createElement(View13, { style: styles13.headerTitleRow }, /* @__PURE__ */ React14.createElement(Text13, { style: styles13.headerTitle }, "BugBear"), testerInfo && /* @__PURE__ */ React14.createElement(TouchableOpacity12, { onPress: () => push({ name: "PROFILE" }) }, /* @__PURE__ */ React14.createElement(Text13, { style: styles13.headerName }, testerInfo.name, " \u270E")))), getHeaderTitle() ? /* @__PURE__ */ React14.createElement(Text13, { style: styles13.headerScreenTitle, numberOfLines: 1 }, getHeaderTitle()) : null, /* @__PURE__ */ React14.createElement(TouchableOpacity12, { onPress: handleClose, style: styles13.closeButton }, /* @__PURE__ */ React14.createElement(Text13, { style: styles13.closeText }, "\u2715"))), /* @__PURE__ */ React14.createElement(
15021
15080
  ScrollView2,
15022
15081
  {
15023
15082
  style: styles13.content,
@@ -15118,15 +15177,27 @@ var styles13 = StyleSheet14.create({
15118
15177
  flex: 1,
15119
15178
  textAlign: "center"
15120
15179
  },
15180
+ headerNavRow: {
15181
+ flexDirection: "row",
15182
+ alignItems: "center",
15183
+ gap: 8
15184
+ },
15121
15185
  backButton: {
15122
15186
  paddingVertical: 2,
15123
- paddingRight: 12
15187
+ paddingRight: 4
15124
15188
  },
15125
15189
  backText: {
15126
15190
  fontSize: 15,
15127
15191
  color: colors.blue,
15128
15192
  fontWeight: "500"
15129
15193
  },
15194
+ homeButton: {
15195
+ paddingVertical: 2,
15196
+ paddingHorizontal: 6
15197
+ },
15198
+ homeText: {
15199
+ fontSize: 16
15200
+ },
15130
15201
  closeButton: {
15131
15202
  width: 32,
15132
15203
  height: 32,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bbearai/react-native",
3
- "version": "0.3.8",
3
+ "version": "0.3.10",
4
4
  "description": "BugBear React Native components for mobile apps",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",