From 8a2043aa157856878e8411baf692903c4f49a8be Mon Sep 17 00:00:00 2001 From: Harsh Mathur Date: Thu, 6 Aug 2026 01:07:52 +0530 Subject: [PATCH] fix: open bug reports from desktop app --- core/feedback/bugReport.ts | 30 ++++++++++++++++++++------ src-tauri/Cargo.lock | 1 + src-tauri/Cargo.toml | 1 + src-tauri/src/feedback.rs | 29 +++++++++++++++++++++++++ src-tauri/src/lib.rs | 2 ++ tests/feedback/bugReport.test.ts | 36 ++++++++++++++++++++++++++++++-- 6 files changed, 91 insertions(+), 8 deletions(-) create mode 100644 src-tauri/src/feedback.rs diff --git a/core/feedback/bugReport.ts b/core/feedback/bugReport.ts index 07f3a07..d05dece 100644 --- a/core/feedback/bugReport.ts +++ b/core/feedback/bugReport.ts @@ -5,6 +5,8 @@ * are the two things a maintainer always needs to reproduce a bug. */ +import { isTauri, safeInvoke } from "@/core/bridge/runtime" + const REPO = "https://github.com/harshmathurx/OpenNotes" const BUG_TEMPLATE = "bug_report.md" @@ -14,7 +16,7 @@ function appVersion(): string { if (typeof process !== "undefined" && process.env?.NEXT_PUBLIC_APP_VERSION) { return process.env.NEXT_PUBLIC_APP_VERSION } - return "0.1.0" + return "0.1.1" } function platform(): string { @@ -27,7 +29,7 @@ function platform(): string { } function isDesktopApp(): boolean { - return typeof window !== "undefined" && "__TAURI_INTERNALS__" in window + return isTauri() } /** @@ -59,8 +61,24 @@ export function buildBugReportURL(extra?: { summary?: string }): string { return `${REPO}/issues/new?${params.toString()}` } -/** Open the report-a-bug flow in a new tab. No-op in non-browser envs. */ -export function openBugReport(): void { - if (typeof window === "undefined") return - window.open(buildBugReportURL(), "_blank", "noopener,noreferrer") +/** Open the report-a-bug flow in the system browser. */ +export async function openBugReport(): Promise { + if (typeof window === "undefined") return false + + const url = buildBugReportURL() + + if (isTauri()) { + try { + await safeInvoke("open_external_url", { url }) + return true + } catch (error) { + console.warn("[opennotes] Failed to open bug report via Tauri", error) + } + } + + const opened = window.open(url, "_blank", "noopener,noreferrer") + if (opened) return true + + window.location.assign(url) + return true } diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index d3227b6..b169ba2 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2031,6 +2031,7 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-dialog", + "url", ] [[package]] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 4494f0e..27cc256 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -18,3 +18,4 @@ tauri-plugin-dialog = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" keyring = "3" +url = "2" diff --git a/src-tauri/src/feedback.rs b/src-tauri/src/feedback.rs new file mode 100644 index 0000000..f60d6c9 --- /dev/null +++ b/src-tauri/src/feedback.rs @@ -0,0 +1,29 @@ +use std::process::Command; + +#[tauri::command] +pub fn open_external_url(url: String) -> Result<(), String> { + let parsed = url::Url::parse(&url).map_err(|_| "Invalid URL".to_string())?; + if parsed.scheme() != "https" { + return Err("Only https URLs can be opened externally".to_string()); + } + + match parsed.host_str() { + Some("github.com") => {} + _ => return Err("Only GitHub URLs can be opened externally".to_string()), + } + + let status = if cfg!(target_os = "macos") { + Command::new("open").arg(&url).status() + } else if cfg!(target_os = "windows") { + Command::new("cmd").args(["/C", "start", "", &url]).status() + } else { + Command::new("xdg-open").arg(&url).status() + } + .map_err(|error| format!("Failed to open URL: {error}"))?; + + if status.success() { + Ok(()) + } else { + Err(format!("Open command failed with status: {status}")) + } +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 31d30dd..6059453 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,3 +1,4 @@ +mod feedback; mod fs; mod git; mod secrets; @@ -7,6 +8,7 @@ pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_dialog::init()) .invoke_handler(tauri::generate_handler![ + feedback::open_external_url, git::run_git, git::git_available, secrets::set_secret, diff --git a/tests/feedback/bugReport.test.ts b/tests/feedback/bugReport.test.ts index 2fcccf6..700247d 100644 --- a/tests/feedback/bugReport.test.ts +++ b/tests/feedback/bugReport.test.ts @@ -1,5 +1,16 @@ -import { describe, it, expect } from "vitest" -import { buildBugReportURL } from "@/core/feedback/bugReport" +import { beforeEach, describe, it, expect, vi } from "vitest" + +const bridge = vi.hoisted(() => ({ + safeInvoke: vi.fn(), + tauri: false, +})) + +vi.mock("@/core/bridge/runtime", () => ({ + isTauri: () => bridge.tauri, + safeInvoke: bridge.safeInvoke, +})) + +import { buildBugReportURL, openBugReport } from "@/core/feedback/bugReport" // URLSearchParams form-encodes spaces as "+"; decode both "+"" and "%20". function bodyOf(url: string): string { @@ -8,6 +19,12 @@ function bodyOf(url: string): string { } describe("buildBugReportURL", () => { + beforeEach(() => { + bridge.tauri = false + bridge.safeInvoke.mockReset() + vi.restoreAllMocks() + }) + it("points at the repo's new-issue page with the bug template", () => { const url = buildBugReportURL() expect(url).toContain("github.com/harshmathurx/OpenNotes/issues/new") @@ -25,4 +42,19 @@ describe("buildBugReportURL", () => { const body = bodyOf(buildBugReportURL({ summary: "sync failed" })) expect(body).toContain("sync failed") }) + + it("opens bug reports through the desktop shell when running in Tauri", async () => { + bridge.tauri = true + bridge.safeInvoke.mockResolvedValue(undefined) + const open = vi.spyOn(window, "open") + + await expect(openBugReport()).resolves.toBe(true) + + expect(bridge.safeInvoke).toHaveBeenCalledWith("open_external_url", { + url: expect.stringContaining( + "github.com/harshmathurx/OpenNotes/issues/new", + ), + }) + expect(open).not.toHaveBeenCalled() + }) })