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:
co-authored by
Claude Fable 5
parent
b5e3c476a5
commit
f07a34c51b
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Pre-open a browser tab inside a click handler, then point it at a URL once
|
||||
* an async result arrives.
|
||||
*
|
||||
* Why: window.open() called after an `await` runs outside the click's
|
||||
* transient user activation. Browsers expire that activation after ~5 seconds
|
||||
* (Safari sooner), so popup blocking kicks in exactly when the request is
|
||||
* slow: a cold serverless start made "Förhandsgranska PDF" silently do
|
||||
* nothing (support: cbysea.se). Opening the tab synchronously keeps the
|
||||
* trusted gesture; the tab is then navigated to the real URL, or closed again
|
||||
* if the request fails. Same pattern as AGIPanel's signing tab.
|
||||
*/
|
||||
|
||||
export interface DeferredTab {
|
||||
/** True when even the synchronous open was blocked (strict popup blocker). */
|
||||
blocked: boolean
|
||||
/** Point the pending tab at the final URL. False if the tab is gone. */
|
||||
navigate(url: string): boolean
|
||||
/** Close the pending tab; call on request failure so no blank tab lingers. */
|
||||
close(): void
|
||||
}
|
||||
|
||||
/**
|
||||
* Must be called synchronously from a user-gesture handler (click), before
|
||||
* any `await`, or the open is popup-blocked in the exact slow cases this
|
||||
* helper exists for.
|
||||
*/
|
||||
export function openDeferredTab(placeholder?: string): DeferredTab {
|
||||
const tab = typeof window !== 'undefined' ? window.open('', '_blank') : null
|
||||
|
||||
if (tab) {
|
||||
try {
|
||||
// Sever the reverse channel up front: the final page never gets a
|
||||
// window.opener, preserving the noopener semantics the direct
|
||||
// window.open(url, '_blank', 'noopener') call sites had.
|
||||
tab.opener = null
|
||||
if (placeholder) {
|
||||
tab.document.title = placeholder
|
||||
const note = tab.document.createElement('p')
|
||||
note.textContent = placeholder
|
||||
note.style.cssText =
|
||||
'font-family: system-ui, sans-serif; color: #666; padding: 24px;'
|
||||
tab.document.body.appendChild(note)
|
||||
}
|
||||
} catch {
|
||||
// The placeholder is cosmetic; never let it break the open.
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
blocked: tab === null,
|
||||
navigate(url: string): boolean {
|
||||
if (!tab || tab.closed) return false
|
||||
try {
|
||||
tab.location.href = url
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
},
|
||||
close() {
|
||||
try {
|
||||
if (tab && !tab.closed) tab.close()
|
||||
} catch {
|
||||
// Already gone; nothing to clean up.
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user