From 98cf39b84743b4c95fc8b3237eeaa9cf540d0633 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Aug 2026 13:11:00 +0000 Subject: [PATCH 01/21] fix(document-viewer): remove the CSP-blocked native PDF reader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Sharper zoom" mode swapped the pdf.js canvas for an iframe pointed at the Supabase signed URL. That can never render in production: buildContentSecurityPolicy sets `default-src 'self'` and declares no frame-src/child-src, so a cross-origin frame inherits 'self' and is refused. It only appeared to work in demo/dev, where the PDF is a same-origin /demo-documents path — which is also all the Playwright suite exercises, so no browser test could see the failure. iOS additionally ignores the #page= fragment the embed relied on for deep links. Remove the reader, its lazy wrapper, and the preference machinery behind it. getDefaultPdfViewerMode() returned false unconditionally, so the 820px breakpoint and the rAF + matchMedia listener that watched it could never change the result — dead work on every viewer mount. "Open PDF" stays as the escape hatch on every platform: a top-level navigation, which CSP does not restrict. Add a source contract asserting no framed reader returns while the CSP has no cross-origin frame-src, since the demo-corpus E2E suite structurally cannot guard this. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01ER2xPFzPzoS8fAxkgHC8yo --- src/components/DocumentViewer.tsx | 67 ++++--------- src/components/document-viewer-lazy.tsx | 2 +- .../non-pdf-source-preview.tsx | 2 +- .../document-viewer/pdf-canvas-viewer.tsx | 30 ------ .../document-viewer/pdf-readers-lazy.tsx | 22 +++-- .../document-viewer/pdf-viewer-mode.ts | 38 -------- .../use-pdf-viewer-preference.ts | 94 ------------------- tests/document-detail-performance.test.ts | 2 +- tests/document-frame-contract.test.ts | 1 - tests/document-viewer-pdf-reader-lazy.test.ts | 42 ++++++--- tests/document-viewer-shell.dom.test.tsx | 1 - tests/ui-hydration.spec.ts | 4 +- tests/ui-smoke.spec.ts | 21 +---- 13 files changed, 70 insertions(+), 256 deletions(-) delete mode 100644 src/components/document-viewer/pdf-viewer-mode.ts delete mode 100644 src/components/document-viewer/use-pdf-viewer-preference.ts diff --git a/src/components/DocumentViewer.tsx b/src/components/DocumentViewer.tsx index 3a88717d21..c2459957fc 100644 --- a/src/components/DocumentViewer.tsx +++ b/src/components/DocumentViewer.tsx @@ -34,7 +34,7 @@ import { textMuted, } from "@/components/ui-primitives"; import { NonPdfSourcePreview } from "@/components/document-viewer/non-pdf-source-preview"; -import { NativePdfEmbed, PdfCanvasViewer } from "@/components/document-viewer/pdf-readers-lazy"; +import { PdfCanvasViewer } from "@/components/document-viewer/pdf-readers-lazy"; import { requestSignedUrlPayload, rowsById, @@ -42,7 +42,6 @@ import { } from "@/components/document-viewer/signed-url-request"; import { useDocumentSummarize } from "@/components/document-viewer/use-document-summarize"; import { useDocumentViewerRoute } from "@/components/document-viewer/use-document-viewer-route"; -import { usePdfViewerPreference } from "@/components/document-viewer/use-pdf-viewer-preference"; import { DocumentFrame, type DocumentFrameControls, type DocumentFrameSource } from "@/components/ui/document-frame"; import { VIEWER_DEFAULT_ZOOM, @@ -210,10 +209,10 @@ export function DocumentViewer({ composerChromeFocused, ); const activeScrollOwner = useActiveScrollOwner(shellScrollContainer, documentId); - const { useNativePdfViewer, togglePdfViewerMode } = usePdfViewerPreference(); - // Phase 2a: DocumentFrame owns zoom/fit/viewing-aid chrome for canvas PDF. - // Reset viewing chrome when the document identity changes (render-time adjust, - // not an effect — avoids react-hooks/set-state-in-effect). + // DocumentFrame owns every viewing control for the canvas PDF — there is one + // reader, so there is one toolbar. Reset viewing chrome when the document + // identity changes (render-time adjust, not an effect — avoids + // react-hooks/set-state-in-effect). const [pdfViewingDocumentId, setPdfViewingDocumentId] = useState(documentId); const [pdfFitWidth, setPdfFitWidth] = useState(true); const [pdfZoom, setPdfZoom] = useState(VIEWER_DEFAULT_ZOOM); @@ -804,7 +803,6 @@ export function DocumentViewer({ const canvasPdfReady = Boolean(signedUrl) && document?.file_type === "application/pdf" && - !useNativePdfViewer && !effectiveLoadingDocument && !effectiveViewerError && !previewError; @@ -1388,46 +1386,21 @@ export function DocumentViewer({ } > {signedUrl && document?.file_type === "application/pdf" ? ( - <> -
- -
- {useNativePdfViewer ? ( - - ) : ( - - )} - + ) : ( ); }); - -function nativePdfEmbedUrl(url: string, initialPage: number) { - const page = Math.max(1, Math.trunc(initialPage || 1)); - return `${url.split("#")[0]}#page=${page}`; -} - -export const NativePdfEmbed = memo(function NativePdfEmbed({ - url, - title, - initialPage, -}: { - url: string; - title: string; - initialPage: number; -}) { - return ( -
-