fix: file viewers blocked on mobile — open blank window before async fetch

Mobile browsers block window.open() after async operations. Changed all
file viewers to open a blank window synchronously in the click handler,
then set location.href after fetch completes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-03-27 10:47:45 +01:00
parent 495fdf6da2
commit e6198e1b67
4 changed files with 24 additions and 6 deletions

View File

@@ -975,18 +975,21 @@ export default function InvoiceDetail() {
// ─── Edit mode: PDF export ─── // ─── Edit mode: PDF export ───
const handleViewPdf = async (_lang = "cs") => { const handleViewPdf = async (_lang = "cs") => {
const newWindow = window.open("", "_blank");
setPdfLoading(true); setPdfLoading(true);
try { try {
const response = await apiFetch(`${API_BASE}/invoices/${id}/file`); const response = await apiFetch(`${API_BASE}/invoices/${id}/file`);
if (!response.ok) { if (!response.ok) {
newWindow?.close();
alert.error("PDF soubor nenalezen — uložte fakturu pro vygenerování"); alert.error("PDF soubor nenalezen — uložte fakturu pro vygenerování");
return; return;
} }
const blob = await response.blob(); const blob = await response.blob();
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
window.open(url, "_blank"); if (newWindow) newWindow.location.href = url;
setTimeout(() => URL.revokeObjectURL(url), 60000); setTimeout(() => URL.revokeObjectURL(url), 60000);
} catch { } catch {
newWindow?.close();
alert.error("Chyba připojení"); alert.error("Chyba připojení");
} finally { } finally {
setPdfLoading(false); setPdfLoading(false);

View File

@@ -768,19 +768,25 @@ export default function OfferDetail() {
const handlePdf = async () => { const handlePdf = async () => {
if (!isEdit || pdfLoading) return; if (!isEdit || pdfLoading) return;
const newWindow = window.open("", "_blank");
setPdfLoading(true); setPdfLoading(true);
try { try {
const response = await apiFetch(`${API_BASE}/offers/${id}/file`); const response = await apiFetch(`${API_BASE}/offers/${id}/file`);
if (response.status === 401) return; if (response.status === 401) {
newWindow?.close();
return;
}
if (!response.ok) { if (!response.ok) {
newWindow?.close();
alert.error("PDF soubor nenalezen — uložte nabídku pro vygenerování"); alert.error("PDF soubor nenalezen — uložte nabídku pro vygenerování");
return; return;
} }
const blob = await response.blob(); const blob = await response.blob();
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
window.open(url, "_blank"); if (newWindow) newWindow.location.href = url;
setTimeout(() => URL.revokeObjectURL(url), 60000); setTimeout(() => URL.revokeObjectURL(url), 60000);
} catch { } catch {
newWindow?.close();
alert.error("Chyba při generování PDF"); alert.error("Chyba při generování PDF");
} finally { } finally {
setPdfLoading(false); setPdfLoading(false);

View File

@@ -221,21 +221,27 @@ export default function Offers() {
const handlePdf = async (quotation: Quotation) => { const handlePdf = async (quotation: Quotation) => {
if (pdfLoading) return; if (pdfLoading) return;
const newWindow = window.open("", "_blank");
setPdfLoading(quotation.id); setPdfLoading(quotation.id);
try { try {
const response = await apiFetch( const response = await apiFetch(
`${API_BASE}/offers/${quotation.id}/file`, `${API_BASE}/offers/${quotation.id}/file`,
); );
if (response.status === 401) return; if (response.status === 401) {
newWindow?.close();
return;
}
if (!response.ok) { if (!response.ok) {
newWindow?.close();
alert.error("PDF soubor nenalezen — otevřete nabídku a uložte ji"); alert.error("PDF soubor nenalezen — otevřete nabídku a uložte ji");
return; return;
} }
const blob = await response.blob(); const blob = await response.blob();
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
window.open(url, "_blank"); if (newWindow) newWindow.location.href = url;
setTimeout(() => URL.revokeObjectURL(url), 60000); setTimeout(() => URL.revokeObjectURL(url), 60000);
} catch { } catch {
newWindow?.close();
alert.error("Chyba připojení"); alert.error("Chyba připojení");
} finally { } finally {
setPdfLoading(null); setPdfLoading(null);

View File

@@ -503,19 +503,22 @@ export default function ReceivedInvoices({
}; };
const openFile = async (inv: ReceivedInvoice) => { const openFile = async (inv: ReceivedInvoice) => {
const newWindow = window.open("", "_blank");
try { try {
const response = await apiFetch( const response = await apiFetch(
`${API_BASE}/received-invoices/${inv.id}/file`, `${API_BASE}/received-invoices/${inv.id}/file`,
); );
if (!response.ok) { if (!response.ok) {
newWindow?.close();
alert.error("Nepodařilo se načíst soubor"); alert.error("Nepodařilo se načíst soubor");
return; return;
} }
const blob = await response.blob(); const blob = await response.blob();
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
window.open(url, "_blank"); if (newWindow) newWindow.location.href = url;
setTimeout(() => URL.revokeObjectURL(url), 60000); setTimeout(() => URL.revokeObjectURL(url), 60000);
} catch { } catch {
newWindow?.close();
alert.error("Chyba připojení"); alert.error("Chyba připojení");
} }
}; };