diff --git a/src/renderer/src/components/mobile/relay-mint-failure-feedback.test.tsx b/src/renderer/src/components/mobile/relay-mint-failure-feedback.test.tsx
new file mode 100644
index 00000000000..4dcc36258ce
--- /dev/null
+++ b/src/renderer/src/components/mobile/relay-mint-failure-feedback.test.tsx
@@ -0,0 +1,54 @@
+// @vitest-environment happy-dom
+
+import { cleanup, render, screen } from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import { afterEach, describe, expect, it, vi } from 'vitest'
+import { useSendRelayMintFailureFeedback } from './relay-mint-failure-feedback'
+import type { MobileRelayMintFailure } from '../../../../shared/mobile-relay-mint-failure'
+
+vi.mock('sonner', () => ({
+ toast: { success: vi.fn(), error: vi.fn() }
+}))
+
+const failure: MobileRelayMintFailure = {
+ code: 'relay_binding_failed',
+ stage: 'binding_failed',
+ message: 'Could not bind the relay session.'
+}
+
+function SendButton(): React.JSX.Element {
+ const send = useSendRelayMintFailureFeedback()
+ return (
+
+ )
+}
+
+afterEach(() => {
+ cleanup()
+ vi.unstubAllGlobals()
+})
+
+describe('useSendRelayMintFailureFeedback', () => {
+ it('drops repeat clicks while a send is in flight so one gh spawn and one report go out', async () => {
+ const user = userEvent.setup()
+ let resolveViewer: (value: null) => void = () => {}
+ const viewer = vi.fn(() => new Promise((resolve) => (resolveViewer = resolve)))
+ const submit = vi.fn(async () => ({ ok: true as const }))
+ vi.stubGlobal(
+ 'window',
+ Object.assign(window, { api: { gh: { viewer }, feedback: { submit } } })
+ )
+
+ render()
+ const button = screen.getByRole('button', { name: 'send' })
+ await user.click(button)
+ await user.click(button)
+ await user.click(button)
+
+ expect(viewer).toHaveBeenCalledTimes(1)
+ resolveViewer(null)
+ await vi.waitFor(() => expect(submit).toHaveBeenCalledTimes(1))
+ })
+})
diff --git a/src/renderer/src/components/mobile/relay-mint-failure-feedback.ts b/src/renderer/src/components/mobile/relay-mint-failure-feedback.ts
index 4e75415d237..c66babb4c1c 100644
--- a/src/renderer/src/components/mobile/relay-mint-failure-feedback.ts
+++ b/src/renderer/src/components/mobile/relay-mint-failure-feedback.ts
@@ -1,4 +1,4 @@
-import { useCallback } from 'react'
+import { useCallback, useRef } from 'react'
import { toast } from 'sonner'
import { translate } from '@/i18n/i18n'
import type { MobileRelayMintFailure } from '../../../../shared/mobile-relay-mint-failure'
@@ -30,7 +30,16 @@ export function buildRelayMintFailureFeedback(args: RelayMintFailureFeedbackArgs
export function useSendRelayMintFailureFeedback(): (
args: RelayMintFailureFeedbackArgs
) => Promise {
+ // Why: the button has no busy state and the send is a `gh` subprocess plus an
+ // HTTPS post, so repeat clicks would file duplicate reports and queue extra
+ // spawns behind the shared gh concurrency limit.
+ const inFlightRef = useRef(false)
+
return useCallback(async (args: RelayMintFailureFeedbackArgs): Promise => {
+ if (inFlightRef.current) {
+ return
+ }
+ inFlightRef.current = true
try {
// Why: identity is best-effort; a missing gh session must not block the report.
const viewer = await window.api.gh.viewer().catch(() => null)
@@ -57,6 +66,8 @@ export function useSendRelayMintFailureFeedback(): (
)
)
console.error('Failed to send relay pairing diagnostics:', error)
+ } finally {
+ inFlightRef.current = false
}
}, [])
}