fix(invoices): stop popup blockers from silently eating the PDF preview tab (#1191)

* fix(invoices): stop popup blockers from silently eating the PDF preview tab

A window.open() after an await runs outside the click's transient user
activation (~5s, less in Safari), so the preview tab was popup-blocked
exactly when generation was slow (cold start + logo re-encode). The
request succeeded, nothing opened, no error: the button looked locked
(support: carina@cbysea.se).

- lib/browser/deferred-tab.ts: open the tab synchronously in the click,
  navigate it when the result arrives, close it on failure (the pattern
  AGIPanel already used for its signing tab), with unit tests.
- InvoiceEditor: preview uses the deferred tab + popup-blocked toast;
  revoke the blob URL instead of leaking it; guard the review dialog
  against an unresolved customer (silent no-op click); 5s timeout on the
  pre-review next-number fetch; spinner + disable while the submit
  handler is in flight in create mode.
- Same pre-open fix in TransactionAttachmentIndicator,
  JournalEntryAttachments (failures now toast instead of vanishing),
  DocumentViewButton, and the Arcim reconnect OAuth popup (its 'trusted
  gesture' comment was wrong after the await).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(invoices): review triage: close blocked preview tab, precise popup hint, test convention

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-07-25 13:43:04 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent b5e3c476a5
commit f07a34c51b
10 changed files with 297 additions and 25 deletions
@@ -1897,6 +1897,17 @@ export default function ArcimMigrationWorkspace(_props: WorkspaceComponentProps)
setIsLoading(true)
setSelectedProvider(provider)
// Pre-open the OAuth popup inside the click's user activation: opening it
// after the fetch below is popup-blocked when the response is slow (the
// activation expires after ~5s). Kept open only for OAuth providers; the
// token path and every failure path close it again. The opener reference
// stays intact: the provider popup posts back via postMessage.
const w = 600
const h = 700
const left = window.screenX + (window.outerWidth - w) / 2
const top = window.screenY + (window.outerHeight - h) / 2
const popup = window.open('', 'arcim-oauth', `width=${w},height=${h},left=${left},top=${top}`)
try {
const res = await fetch('/api/extensions/ext/arcim-migration/connect', {
method: 'POST',
@@ -1913,19 +1924,24 @@ export default function ArcimMigrationWorkspace(_props: WorkspaceComponentProps)
setAuthType(data.authType)
if (data.authType === 'oauth' && data.authUrl) {
// Open immediately: this runs inside the button's click handler, so
// the popup is a trusted user gesture and won't be blocked.
const w = 600
const h = 700
const left = window.screenX + (window.outerWidth - w) / 2
const top = window.screenY + (window.outerHeight - h) / 2
window.open(data.authUrl, 'arcim-oauth', `width=${w},height=${h},left=${left},top=${top}`)
if (popup && !popup.closed) {
popup.location.href = data.authUrl
} else {
// The pre-opened popup was blocked or closed; retrying here is a
// long shot (the activation may be gone) but strictly better than
// dropping the flow.
window.open(data.authUrl, 'arcim-oauth', `width=${w},height=${h},left=${left},top=${top}`)
}
setAuthUrl(data.authUrl)
} else if (data.authType === 'token') {
// Re-enter credentials for token-based providers
setStep('connect')
} else {
popup?.close()
if (data.authType === 'token') {
// Re-enter credentials for token-based providers
setStep('connect')
}
}
} catch (err) {
popup?.close()
setError(err instanceof Error ? getUserErrorMessage(err) : 'Kunde inte återansluta')
setAuthExpired(true)
} finally {