mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix(sign-pdf): surface PDF load failures instead of a blank canvas (#545)
A genuine pdf.js load failure (corrupt or password-protected file) was swallowed by the same catch that silences teardown rejections, leaving a blank canvas that looks like it is still loading. The two cases are now distinguished by the load effect's cancelled flag, and a real failure renders a clear message pointing at Unlock PDF for encrypted files. New loadFailed string in all 21 locales. Item 4 of #478.
This commit is contained in:
@@ -68,6 +68,7 @@ export const SignCanvas = forwardRef<SignCanvasRef, Props>(function SignCanvas(
|
||||
// the first page, since setPage(0) is a no-op when page is already 0 (and
|
||||
// pageCount never changes for single-page PDFs).
|
||||
const [docReady, setDocReady] = useState(false);
|
||||
const [loadFailed, setLoadFailed] = useState(false);
|
||||
|
||||
// Load the document once per file. A replaced file starts clean: drop the
|
||||
// previous document's placements and stage so they don't carry over.
|
||||
@@ -75,6 +76,7 @@ export const SignCanvas = forwardRef<SignCanvasRef, Props>(function SignCanvas(
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
setDocReady(false);
|
||||
setLoadFailed(false);
|
||||
for (const placed of placementsRef.current) placed.node.destroy();
|
||||
placementsRef.current = [];
|
||||
pageMetaRef.current.clear();
|
||||
@@ -92,9 +94,11 @@ export const SignCanvas = forwardRef<SignCanvasRef, Props>(function SignCanvas(
|
||||
setPage(0);
|
||||
setDocReady(true);
|
||||
})().catch(() => {
|
||||
// loadingTask.destroy() in the cleanup rejects the in-flight promise;
|
||||
// swallowing it is the fix for Sentry NODE-1N. A genuine load failure
|
||||
// leaves docReady false, which the existing UI already handles.
|
||||
// loadingTask.destroy() in the cleanup rejects the in-flight promise
|
||||
// (Sentry NODE-1N); `cancelled` is already true then, so stay silent. A
|
||||
// genuine load failure (corrupt or password-protected file) must surface
|
||||
// instead of leaving a blank canvas that looks like it is still loading.
|
||||
if (!cancelled) setLoadFailed(true);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
@@ -324,6 +328,16 @@ export const SignCanvas = forwardRef<SignCanvasRef, Props>(function SignCanvas(
|
||||
[page, size],
|
||||
);
|
||||
|
||||
if (loadFailed) {
|
||||
return (
|
||||
<div className="flex flex-1 items-center justify-center p-8">
|
||||
<p className="max-w-md text-center text-sm text-destructive">
|
||||
{t.toolSettings["sign-pdf"].loadFailed}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col items-center gap-3 p-4">
|
||||
<div className="relative" style={{ width: size.w, height: size.h }}>
|
||||
|
||||
@@ -2796,6 +2796,8 @@ export const ar: TranslationKeys = {
|
||||
signingPercent: "جارٍ التوقيع… {percent}%",
|
||||
downloadSigned: "تنزيل ملف PDF الموقَّع",
|
||||
addFirst: "أضف توقيعًا واحدًا على الأقل قبل التنزيل.",
|
||||
loadFailed:
|
||||
"تعذّر عرض ملف PDF هذا. قد يكون الملف تالفًا أو محميًا بكلمة مرور؛ إذا كان مشفّرًا، فأزل كلمة المرور أولًا باستخدام أداة فتح قفل PDF.",
|
||||
pad: {
|
||||
title: "إنشاء توقيع",
|
||||
draw: "رسم",
|
||||
|
||||
@@ -2821,6 +2821,8 @@ export const de: TranslationKeys = {
|
||||
signingPercent: "Wird unterschrieben… {percent}%",
|
||||
downloadSigned: "Unterschriebenes PDF herunterladen",
|
||||
addFirst: "Fügen Sie vor dem Herunterladen mindestens eine Unterschrift hinzu.",
|
||||
loadFailed:
|
||||
"Dieses PDF konnte nicht angezeigt werden. Die Datei ist möglicherweise beschädigt oder passwortgeschützt; falls sie verschlüsselt ist, entfernen Sie das Passwort zuerst mit dem Werkzeug „PDF entsperren“.",
|
||||
pad: {
|
||||
title: "Unterschrift erstellen",
|
||||
draw: "Zeichnen",
|
||||
|
||||
@@ -2761,6 +2761,8 @@ export const en = {
|
||||
signingPercent: "Signing… {percent}%",
|
||||
downloadSigned: "Download signed PDF",
|
||||
addFirst: "Add at least one signature before downloading.",
|
||||
loadFailed:
|
||||
"Couldn't display this PDF. The file may be corrupted or password-protected; if it's encrypted, remove the password first with the Unlock PDF tool.",
|
||||
pad: {
|
||||
title: "Create signature",
|
||||
draw: "Draw",
|
||||
|
||||
@@ -2801,6 +2801,8 @@ export const es: TranslationKeys = {
|
||||
signingPercent: "Firmando… {percent}%",
|
||||
downloadSigned: "Descargar PDF firmado",
|
||||
addFirst: "Añade al menos una firma antes de descargar.",
|
||||
loadFailed:
|
||||
"No se pudo mostrar este PDF. El archivo puede estar dañado o protegido con contraseña; si está cifrado, quita primero la contraseña con la herramienta Desbloquear PDF.",
|
||||
pad: {
|
||||
title: "Crear firma",
|
||||
draw: "Dibujar",
|
||||
|
||||
@@ -2827,6 +2827,8 @@ export const fr: TranslationKeys = {
|
||||
signingPercent: "Signature en cours… {percent}%",
|
||||
downloadSigned: "Télécharger le PDF signé",
|
||||
addFirst: "Ajoutez au moins une signature avant de télécharger.",
|
||||
loadFailed:
|
||||
"Impossible d'afficher ce PDF. Le fichier est peut-être corrompu ou protégé par mot de passe ; s'il est chiffré, retirez d'abord le mot de passe avec l'outil Déverrouiller le PDF.",
|
||||
pad: {
|
||||
title: "Créer une signature",
|
||||
draw: "Dessiner",
|
||||
|
||||
@@ -2626,6 +2626,8 @@ export const hi: TranslationKeys = {
|
||||
signingPercent: "हस्ताक्षर हो रहे हैं… {percent}%",
|
||||
downloadSigned: "हस्ताक्षरित PDF डाउनलोड करें",
|
||||
addFirst: "डाउनलोड करने से पहले कम से कम एक हस्ताक्षर जोड़ें।",
|
||||
loadFailed:
|
||||
"यह PDF प्रदर्शित नहीं हो सका। फ़ाइल दूषित या पासवर्ड से सुरक्षित हो सकती है; यदि यह एन्क्रिप्टेड है, तो पहले PDF अनलॉक टूल से पासवर्ड हटाएँ।",
|
||||
pad: {
|
||||
title: "हस्ताक्षर बनाएं",
|
||||
draw: "बनाएं",
|
||||
|
||||
@@ -2810,6 +2810,8 @@ export const id: TranslationKeys = {
|
||||
signingPercent: "Menandatangani… {percent}%",
|
||||
downloadSigned: "Unduh PDF yang ditandatangani",
|
||||
addFirst: "Tambahkan setidaknya satu tanda tangan sebelum mengunduh.",
|
||||
loadFailed:
|
||||
"Tidak dapat menampilkan PDF ini. File mungkin rusak atau dilindungi kata sandi; jika terenkripsi, hapus kata sandinya terlebih dahulu dengan alat Buka Kunci PDF.",
|
||||
pad: {
|
||||
title: "Buat tanda tangan",
|
||||
draw: "Gambar",
|
||||
|
||||
@@ -2815,6 +2815,8 @@ export const it: TranslationKeys = {
|
||||
signingPercent: "Firma in corso… {percent}%",
|
||||
downloadSigned: "Scarica il PDF firmato",
|
||||
addFirst: "Aggiungi almeno una firma prima di scaricare.",
|
||||
loadFailed:
|
||||
"Impossibile visualizzare questo PDF. Il file potrebbe essere danneggiato o protetto da password; se è crittografato, rimuovi prima la password con lo strumento Sblocca PDF.",
|
||||
pad: {
|
||||
title: "Crea firma",
|
||||
draw: "Disegna",
|
||||
|
||||
@@ -2769,6 +2769,8 @@ export const ja: TranslationKeys = {
|
||||
signingPercent: "署名中… {percent}%",
|
||||
downloadSigned: "署名済み PDF をダウンロード",
|
||||
addFirst: "ダウンロードする前に署名を 1 つ以上追加してください。",
|
||||
loadFailed:
|
||||
"このPDFを表示できませんでした。ファイルが破損しているか、パスワードで保護されている可能性があります。暗号化されている場合は、先にPDFロック解除ツールでパスワードを解除してください。",
|
||||
pad: {
|
||||
title: "署名を作成",
|
||||
draw: "手書き",
|
||||
|
||||
@@ -2749,6 +2749,8 @@ export const ko: TranslationKeys = {
|
||||
signingPercent: "서명 중… {percent}%",
|
||||
downloadSigned: "서명된 PDF 다운로드",
|
||||
addFirst: "다운로드하기 전에 서명을 하나 이상 추가하세요.",
|
||||
loadFailed:
|
||||
"이 PDF를 표시할 수 없습니다. 파일이 손상되었거나 비밀번호로 보호되어 있을 수 있습니다. 암호화된 경우 먼저 PDF 잠금 해제 도구로 비밀번호를 제거하세요.",
|
||||
pad: {
|
||||
title: "서명 만들기",
|
||||
draw: "그리기",
|
||||
|
||||
@@ -2816,6 +2816,8 @@ export const nl: TranslationKeys = {
|
||||
signingPercent: "Bezig met ondertekenen… {percent}%",
|
||||
downloadSigned: "Ondertekende PDF downloaden",
|
||||
addFirst: "Voeg ten minste één handtekening toe voordat je downloadt.",
|
||||
loadFailed:
|
||||
"Deze PDF kan niet worden weergegeven. Het bestand is mogelijk beschadigd of met een wachtwoord beveiligd; als het versleuteld is, verwijder dan eerst het wachtwoord met de tool PDF ontgrendelen.",
|
||||
pad: {
|
||||
title: "Handtekening maken",
|
||||
draw: "Tekenen",
|
||||
|
||||
@@ -2814,6 +2814,8 @@ export const pl: TranslationKeys = {
|
||||
signingPercent: "Podpisywanie… {percent}%",
|
||||
downloadSigned: "Pobierz podpisany PDF",
|
||||
addFirst: "Dodaj co najmniej jeden podpis przed pobraniem.",
|
||||
loadFailed:
|
||||
"Nie udało się wyświetlić tego pliku PDF. Plik może być uszkodzony lub chroniony hasłem; jeśli jest zaszyfrowany, najpierw usuń hasło narzędziem Odblokuj PDF.",
|
||||
pad: {
|
||||
title: "Utwórz podpis",
|
||||
draw: "Rysuj",
|
||||
|
||||
@@ -2812,6 +2812,8 @@ export const ptBR: TranslationKeys = {
|
||||
signingPercent: "Assinando… {percent}%",
|
||||
downloadSigned: "Baixar PDF assinado",
|
||||
addFirst: "Adicione pelo menos uma assinatura antes de baixar.",
|
||||
loadFailed:
|
||||
"Não foi possível exibir este PDF. O arquivo pode estar corrompido ou protegido por senha; se estiver criptografado, remova a senha primeiro com a ferramenta Desbloquear PDF.",
|
||||
pad: {
|
||||
title: "Criar assinatura",
|
||||
draw: "Desenhar",
|
||||
|
||||
@@ -2812,6 +2812,8 @@ export const ru: TranslationKeys = {
|
||||
signingPercent: "Подписание… {percent}%",
|
||||
downloadSigned: "Скачать подписанный PDF",
|
||||
addFirst: "Добавьте хотя бы одну подпись перед скачиванием.",
|
||||
loadFailed:
|
||||
"Не удалось отобразить этот PDF. Файл может быть повреждён или защищён паролем; если он зашифрован, сначала удалите пароль с помощью инструмента «Разблокировать PDF».",
|
||||
pad: {
|
||||
title: "Создать подпись",
|
||||
draw: "Нарисовать",
|
||||
|
||||
@@ -2809,6 +2809,8 @@ export const sv: TranslationKeys = {
|
||||
signingPercent: "Signerar… {percent}%",
|
||||
downloadSigned: "Ladda ner signerad PDF",
|
||||
addFirst: "Lägg till minst en signatur innan du laddar ner.",
|
||||
loadFailed:
|
||||
"Det gick inte att visa denna PDF. Filen kan vara skadad eller lösenordsskyddad; om den är krypterad, ta först bort lösenordet med verktyget Lås upp PDF.",
|
||||
pad: {
|
||||
title: "Skapa signatur",
|
||||
draw: "Rita",
|
||||
|
||||
@@ -2780,6 +2780,8 @@ export const th: TranslationKeys = {
|
||||
signingPercent: "กำลังลงนาม… {percent}%",
|
||||
downloadSigned: "ดาวน์โหลด PDF ที่ลงนามแล้ว",
|
||||
addFirst: "เพิ่มลายเซ็นอย่างน้อยหนึ่งรายการก่อนดาวน์โหลด",
|
||||
loadFailed:
|
||||
"ไม่สามารถแสดง PDF นี้ได้ ไฟล์อาจเสียหายหรือมีการป้องกันด้วยรหัสผ่าน หากถูกเข้ารหัส โปรดลบรหัสผ่านด้วยเครื่องมือปลดล็อก PDF ก่อน",
|
||||
pad: {
|
||||
title: "สร้างลายเซ็น",
|
||||
draw: "วาด",
|
||||
|
||||
@@ -2813,6 +2813,8 @@ export const tr: TranslationKeys = {
|
||||
signingPercent: "İmzalanıyor… {percent}%",
|
||||
downloadSigned: "İmzalı PDF'yi indir",
|
||||
addFirst: "İndirmeden önce en az bir imza ekleyin.",
|
||||
loadFailed:
|
||||
"Bu PDF görüntülenemedi. Dosya bozuk veya parola korumalı olabilir; şifrelenmişse önce PDF Kilidini Aç aracıyla parolayı kaldırın.",
|
||||
pad: {
|
||||
title: "İmza oluştur",
|
||||
draw: "Çiz",
|
||||
|
||||
@@ -2811,6 +2811,8 @@ export const uk: TranslationKeys = {
|
||||
signingPercent: "Підписування… {percent}%",
|
||||
downloadSigned: "Завантажити підписаний PDF",
|
||||
addFirst: "Додайте принаймні один підпис перед завантаженням.",
|
||||
loadFailed:
|
||||
"Не вдалося відобразити цей PDF. Файл може бути пошкоджений або захищений паролем; якщо він зашифрований, спочатку видаліть пароль інструментом «Розблокувати PDF».",
|
||||
pad: {
|
||||
title: "Створити підпис",
|
||||
draw: "Намалювати",
|
||||
|
||||
@@ -2811,6 +2811,8 @@ export const vi: TranslationKeys = {
|
||||
signingPercent: "Đang ký… {percent}%",
|
||||
downloadSigned: "Tải PDF đã ký",
|
||||
addFirst: "Thêm ít nhất một chữ ký trước khi tải xuống.",
|
||||
loadFailed:
|
||||
"Không thể hiển thị PDF này. Tệp có thể bị hỏng hoặc được bảo vệ bằng mật khẩu; nếu tệp được mã hóa, hãy xóa mật khẩu bằng công cụ Mở khóa PDF trước.",
|
||||
pad: {
|
||||
title: "Tạo chữ ký",
|
||||
draw: "Vẽ",
|
||||
|
||||
@@ -2568,6 +2568,8 @@ export const zhCN: TranslationKeys = {
|
||||
signingPercent: "正在签名… {percent}%",
|
||||
downloadSigned: "下载已签名的 PDF",
|
||||
addFirst: "下载前请至少添加一个签名。",
|
||||
loadFailed:
|
||||
"无法显示此 PDF。文件可能已损坏或受密码保护;如果已加密,请先使用“PDF 解锁”工具移除密码。",
|
||||
pad: {
|
||||
title: "创建签名",
|
||||
draw: "手写",
|
||||
|
||||
@@ -2568,6 +2568,8 @@ export const zhTW: TranslationKeys = {
|
||||
signingPercent: "正在簽署… {percent}%",
|
||||
downloadSigned: "下載已簽署的 PDF",
|
||||
addFirst: "下載前請至少新增一個簽名。",
|
||||
loadFailed:
|
||||
"無法顯示此 PDF。檔案可能已損毀或受密碼保護;如果已加密,請先使用「解鎖 PDF」工具移除密碼。",
|
||||
pad: {
|
||||
title: "建立簽名",
|
||||
draw: "手寫",
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
// A genuine pdf.js load failure (corrupt or password-protected file). The
|
||||
// teardown-path rejection (loadingTask.destroy() mid-flight) is a separate,
|
||||
// expected path and must stay silent; see #478.
|
||||
vi.mock("pdfjs-dist", () => ({
|
||||
GlobalWorkerOptions: {},
|
||||
getDocument: () => ({ promise: Promise.reject(new Error("Invalid PDF structure")) }),
|
||||
}));
|
||||
|
||||
import { SignCanvas } from "@/components/tools/sign-canvas";
|
||||
|
||||
afterEach(cleanup);
|
||||
|
||||
describe("SignCanvas load failure", () => {
|
||||
it("shows a visible error instead of a blank canvas when the PDF cannot load", async () => {
|
||||
render(<SignCanvas fileUrl="blob:bad-pdf" />);
|
||||
|
||||
expect(await screen.findByText(/couldn't display this pdf/i)).toBeInTheDocument();
|
||||
// The dead canvas and page controls are gone; the user is not left staring
|
||||
// at an empty page that looks like it is still loading.
|
||||
expect(screen.queryByTestId("sign-pdf-canvas")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -125,6 +125,7 @@ export default defineConfig({
|
||||
"@/components/navbar": path.resolve(__dirname, "apps/landing/src/components/navbar"),
|
||||
"@": path.resolve(__dirname, "apps/web/src"),
|
||||
"framer-motion": path.join(landingNodeModules, "framer-motion"),
|
||||
"pdfjs-dist": path.join(webNodeModules, "pdfjs-dist"),
|
||||
"@snapotter/ai": path.resolve(__dirname, "packages/ai/src/index.ts"),
|
||||
"@snapotter/enterprise": path.resolve(__dirname, "packages/enterprise/src/index.ts"),
|
||||
"@snapotter/image-engine": path.resolve(__dirname, "packages/image-engine/src/index.ts"),
|
||||
|
||||
Reference in New Issue
Block a user