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