From 9c3ce9dd7d26e3aeabfb33a872f229927412c35a Mon Sep 17 00:00:00 2001 From: Jakub Kasprzyk Date: Wed, 21 Oct 2026 00:24:19 +0200 Subject: [PATCH] feat: reject ref and PdfDocument methods with coded PdfMethodErrors The view's promise methods (getOutline, getPageSizes, renderPage, print, getFormFields, setFormFieldValues, exportDocument) and PdfDocument's reads rejected with plain Errors: no code to match on, messages that differed between platforms, and on Android the exception class and the whole stack trace in the message. They now reject with a PdfMethodError whose code (PdfMethodErrorCode) is the same on both platforms: not-loaded, closed, invalid-argument, not-supported, not-permitted, busy, io-error, invalid-pdf, out-of-memory or unknown. message is one sentence; nativeDescription keeps the platform's text. Nitro rejections carry only a message, so native code rejects with "[code] message" (PdfMethodFailure in Swift, PdfMethodException in Kotlin) and one JS function parses its first line, after Android's exception class (renamed in minified builds) and before the stack trace. Platform errors are coded by type: file system errors are io-error, OutOfMemoryError out-of-memory, parse failures invalid-pdf, and the outline and metadata of encrypted documents on Android not-supported. The Nitro specs are unchanged. The same failure has the same code: a single-choice list box given several options is invalid-argument on iOS too (it was "not supported on iOS"), and Android's renderPage width message no longer reads "4096.0". On iOS, a read or export still waiting for its queue when the view closes or its source changes rejects with closed, as on Android, instead of resolving with the old document's data. The example's event lines show code and message, and the e2e flows match codes instead of message regexes. Behavior change: on Android, error.message no longer holds the exception class and the stack trace (nativeDescription does). Apps that matched on message text should match on code. Closes #65 --- ARCHITECTURE.md | 3 + README.md | 2 +- docs/api-design.md | 34 ++++++ docs/api.md | 27 +++++ docs/guides/forms.md | 8 +- docs/guides/page-images-and-documents.md | 3 +- docs/guides/printing.md | 17 +-- docs/guides/sources-and-loading.md | 3 +- docs/limitations.md | 4 +- example/describeError.ts | 8 ++ example/e2e/pdf-document.yaml | 4 +- example/e2e/stress-close-during-work.yaml | 2 +- example/e2e/viewer-forms.yaml | 6 +- example/e2e/viewer-sources.yaml | 2 +- example/screens/DocumentsScreen.tsx | 9 +- example/screens/FormBar.tsx | 13 +- example/screens/ViewerScreen.tsx | 11 +- .../nitro/betterpdf/Bitmap+writeToCache.kt | 3 +- .../nitro/betterpdf/DocumentReads+js.kt | 4 +- .../margelo/nitro/betterpdf/DocumentReads.kt | 4 +- .../nitro/betterpdf/DocumentSources.kt | 2 +- .../nitro/betterpdf/FormFieldEntry+values.kt | 17 ++- .../betterpdf/HybridBetterPdfDocument.kt | 12 +- .../nitro/betterpdf/HybridBetterPdfView.kt | 41 ++++--- .../nitro/betterpdf/PageImageRequest.kt | 8 +- .../nitro/betterpdf/PdfDocumentSession.kt | 26 ++-- .../nitro/betterpdf/PdfMethodException.kt | 59 +++++++++ .../margelo/nitro/betterpdf/SerialWorker.kt | 5 +- .../betterpdf/outline/PdfDocumentReader.kt | 2 +- .../betterpdf/outline/PdfFormatException.kt | 3 + .../margelo/nitro/betterpdf/FormValuesTest.kt | 29 +++-- .../nitro/betterpdf/PdfDocumentSessionTest.kt | 5 +- .../nitro/betterpdf/PdfMethodExceptionTest.kt | 53 +++++++++ package/ios/DisplayedDocument.swift | 14 +++ package/ios/DocumentForm+setValues.swift | 22 ++-- package/ios/HybridBetterPdfDocument.swift | 8 +- package/ios/HybridBetterPdfView.swift | 83 +++++++------ package/ios/PDFPage+renderImage.swift | 5 +- package/ios/PageImageRequest.swift | 9 +- package/ios/PdfMethodFailure.swift | 49 ++++++++ package/src/PdfDocument.test.ts | 41 ++++++- package/src/PdfDocument.ts | 22 ++-- package/src/PdfMethodError.test.ts | 112 ++++++++++++++++++ package/src/PdfMethodError.ts | 83 +++++++++++++ package/src/PdfView.test.tsx | 47 ++++++++ package/src/PdfView.tsx | 84 +++++++------ package/src/formFields.ts | 4 +- package/src/index.ts | 2 + package/src/types/PdfMethodErrorCode.ts | 19 +++ 49 files changed, 817 insertions(+), 216 deletions(-) create mode 100644 example/describeError.ts create mode 100644 package/android/src/main/java/com/margelo/nitro/betterpdf/PdfMethodException.kt create mode 100644 package/android/src/test/java/com/margelo/nitro/betterpdf/PdfMethodExceptionTest.kt create mode 100644 package/ios/PdfMethodFailure.swift create mode 100644 package/src/PdfMethodError.test.ts create mode 100644 package/src/PdfMethodError.ts create mode 100644 package/src/types/PdfMethodErrorCode.ts diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 0c008af..4dd43c2 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -42,6 +42,7 @@ package/ PdfView.tsx public component (wrapper over the host component) PdfCache.ts public cache API: AbortSignal, PdfDownloadError PdfDocument.ts public document API: open (AbortSignal, PdfOpenError), close + PdfMethodError.ts methods' rejections: native `[code] message` → PdfMethodError source.ts PdfSource union (or a list) → native [{ uri, base64, headers, cache options, password }] nativeSource.ts base64 text → native-owned ArrayBuffer (once per distinct text) test/base64/ the C++ and Swift base64 decoders against shared cases, and each other @@ -56,6 +57,7 @@ package/ DocumentSetLoader.swift the view's sources in parallel; several assembled into one PDFDocument DisplayedDocument.swift the displayed document and its sources (files, passwords, first pages) PdfLoadFailure.swift Swift error → PdfError + PdfMethodFailure.swift a method's rejection as `[code] message` DocumentCache.swift fresh copy or shared download/revalidation, stale fallback DocumentCacheStore.swift copies on disk: session/persistent dirs, metadata, pins SharedDownload.swift one URLSession request per copy, conditional with a copy @@ -93,6 +95,7 @@ package/ DocumentReads.kt outline, metadata, page sizes, page images on duplicated FDs (view session and document) PageImageRequest.kt renderPage validation and file write (view and document) + PdfMethodException.kt a method's rejection as `[code] message` outline/ bounded PDF parser: outline, page tree, /Info (PdfDate) android/src/main/cpp/ cpp-adapter.cpp JNI_OnLoad: Nitro's natives, then NativeBase64Document's and NativePageCoverage's diff --git a/README.md b/README.md index 9fbeb4b..7082a5e 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ PDFKit on iOS, the platform `PdfRenderer` on Android: no bundled PDFium, no WebV - **Forms** — AcroForm fields filled in by touch or from code, read back by name and exported as a filled-in PDF. - **Page overlays** — React views attached to points of a page, moved natively while it scrolls and zooms. - **Document APIs** — outline, page sizes, metadata, page images and printing, with or without a view. -- **Password protection, typed errors, accessibility** — stable error codes identical on both platforms; VoiceOver and TalkBack read each page. +- **Password protection, typed errors, accessibility** — stable error codes identical on both platforms, for load failures and method calls alike; VoiceOver and TalkBack read each page. ## Requirements diff --git a/docs/api-design.md b/docs/api-design.md index 652a37f..4966d2a 100644 --- a/docs/api-design.md +++ b/docs/api-design.md @@ -266,6 +266,40 @@ type PdfErrorCode = `error.nativeDescription` carries the raw platform message for logs. +The ref's promise methods and `PdfDocument`'s reads reject with a +`PdfMethodError` and codes of their own: + +```ts +type PdfMethodErrorCode = + | 'not-loaded' // before onLoad, while a new source loads + | 'closed' // closed or replaced while the call waited + | 'invalid-argument' // page, width, form value, export path + | 'not-supported' // several sources, a platform limit + | 'not-permitted' // printing forbidden, read-only field + | 'busy' // a print dialog is open + | 'io-error' + | 'invalid-pdf' // outline, metadata or form not parsable + | 'out-of-memory' + | 'unknown'; // see nativeDescription +``` + +- **A union of its own.** Adding these to `PdfErrorCode` would break apps + that switch exhaustively over `onError`'s codes. +- **A coded message, not result structs.** Nitro rejections carry only a + message. `open` and `download` resolve a result struct instead, but that + would have changed 11 signatures and every future method. Native code + rejects with `[code] message` (`PdfMethodFailure` in Swift, + `PdfMethodException` in Kotlin), and one JS function parses the first + line: Android puts the exception's class before it (renamed in minified + release builds) and the stack trace after it. A rejection without a + known code is `unknown`, the platform's text in `nativeDescription`. +- **Platform errors get a code by type**: file system errors are + `io-error` on both platforms, `OutOfMemoryError` `out-of-memory`, a parse + failure `invalid-pdf`. +- In 1.1 these methods rejected with plain `Error`s: on Android the message + was the exception's class, message and stack trace, and the same failure + read differently on each platform, so apps could only match on text. + ## What we omit from react-native-pdf, and why | react-native-pdf | Decision | Reason | diff --git a/docs/api.md b/docs/api.md index 80eb860..b88105e 100644 --- a/docs/api.md +++ b/docs/api.md @@ -75,6 +75,9 @@ each page. The viewer adds no safe-area insets on either platform, like | `search(query)` / `searchNext()` / `searchPrevious()` / `clearSearch()` | Full-text search. `search` is ignored before `onLoad` | | `getStats()` | Render and cache statistics, for diagnostics | +The promise methods reject with a `PdfMethodError`; see +[Method errors](#method-errors). + `goToPage`, `zoomTo` and `goToViewport` ignore `NaN` and `±Infinity`, e.g. `goToPage(Number(text) - 1)` while a page field holds a letter. A fractional page is rounded to the nearest page on both platforms: `goToPage(1.6)` opens @@ -115,6 +118,7 @@ pdf.current?.goToViewport(saved); // a position from onViewportChan | `PdfCache` | Stored downloads: `download()`, `getEntry()`, `remove()`, `getByteSize()`, `clear()` | | `PdfOpenError` | Rejection of `PdfDocument.open`, with the view's error `code` | | `PdfDownloadError` | Rejection of `PdfCache.download`, with a `code` and `httpStatus` | +| `PdfMethodError` | Rejection of the ref's promise methods and `PdfDocument`'s reads, with a `code` | ## Errors @@ -131,6 +135,29 @@ carries the platform error for logging. Load failures carry source; unset when the set failed as a whole, as when Android runs out of memory laying out many sources' pages). +### Method errors + +The ref's promise methods (`getOutline`, `getPageSizes`, `renderPage`, +`print`, `getFormFields`, `setFormFieldValues`, `exportDocument`) and +`PdfDocument`'s reads reject with a `PdfMethodError`. Its `code` +(`PdfMethodErrorCode`) is stable and identical on both platforms: + +| Code | When | +| --- | --- | +| `not-loaded` | Before `onLoad`, also while a new `source` loads; before the view mounts | +| `closed` | `PdfDocument` after `close()`; the view closed or its `source` changed while the call waited for its turn (a call already running finishes) | +| `invalid-argument` | `renderPage`: a page outside the document, a `width` outside 1–4096 px, an image over 4096 × 4096 px. `setFormFieldValues`: an unknown field, a value its type does not take, text over its maximum length, an option it does not have. `exportDocument`: a `filePath` that is a directory, the displayed file or in the download cache | +| `not-supported` | Several sources (`print`, the form methods, `exportDocument`); a device that cannot print (iOS); filling forms on Android below 15 without SDK extension 13; selecting several options of a list box on iOS; clearing a radio group that cannot be turned off on Android; the outline and metadata of encrypted documents on Android | +| `not-permitted` | The document does not permit printing (iOS); a read-only form field | +| `busy` | A print dialog of the view is open; on Android, the app is not in the foreground | +| `io-error` | A file could not be read or written: a copy of the document, an export, a page image. The print job failed | +| `invalid-pdf` | The outline, metadata or form could not be parsed | +| `out-of-memory` | Out of memory, e.g. while rendering a page image | +| `unknown` | Anything else; see `nativeDescription` | + +`error.message` is one sentence and may change; `error.nativeDescription` +is the platform's text (on Android with the stack trace), for logging. + ## Types | Type | Description | diff --git a/docs/guides/forms.md b/docs/guides/forms.md index e5bf3de..2a7ac7e 100644 --- a/docs/guides/forms.md +++ b/docs/guides/forms.md @@ -27,7 +27,7 @@ const { filePath } = await pdf.current!.exportDocument(); // share or upload it over the form field (kept above the keyboard) and a list to pick choices from. `enableFormFilling={false}` leaves them as they are. - A view showing several sources has no form to fill in: fields are drawn - but not editable, and the form methods reject (see + but not editable, and the form methods reject with `not-supported` (see [Several documents as one](sources-and-loading.md#several-documents-as-one)). - `onFormChange` reports the reader's changes only — at once for a toggle or choice, when editing ends for text, never per keystroke — not @@ -37,14 +37,14 @@ const { filePath } = await pdf.current!.exportDocument(); // share or upload it coordinates as `onPageTap` — e.g. to place a `PdfPageOverlay` on a signature field. Choices are their displayed labels. - `setFormFieldValues()` sets fields by name, all or none: it rejects, - changing nothing, for an unknown or read-only field or a value its type - does not take. + changing nothing, for an unknown field or a value its type does not take + (`invalid-argument`) and for a read-only field (`not-permitted`). - `exportDocument()` writes the document as shown to a new file (the cache directory by default, or `{ filePath }`, which must name a file, not a directory), keeping its password protection, off the main thread; the source file is never changed. A remote document's `onLoad` `filePath` is the library's downloaded copy: exporting over it, or - anywhere else in that cache, is rejected. + anywhere else in that cache, is rejected (`invalid-argument`). Printing and `renderPage` show the form as filled in too. - Password protection of an export: the password that opened the document opens the export, with the same permissions (printing, copying, form diff --git a/docs/guides/page-images-and-documents.md b/docs/guides/page-images-and-documents.md index 82de5ff..4fb4609 100644 --- a/docs/guides/page-images-and-documents.md +++ b/docs/guides/page-images-and-documents.md @@ -68,4 +68,5 @@ try { once renders them one at a time. - **Call `close()`** when done: an open document keeps its file open until then (or until the JS garbage collector releases it, which can take - long). Calls after `close()` reject. + long). Calls after `close()` reject with a `PdfMethodError` whose `code` + is `closed`. diff --git a/docs/guides/printing.md b/docs/guides/printing.md index 5c98db3..bb8599e 100644 --- a/docs/guides/printing.md +++ b/docs/guides/printing.md @@ -14,14 +14,15 @@ if (result?.isSubmitted) showToast('Sent to the printer'); Works for `content://` sources, which have no file path. - `jobName` names the job in the print queue and the file when saving as PDF; by default the source's file name, or `'Document'`. -- Rejects before `onLoad`, while the view's print dialog is open, when the - view closes or its `source` changes before the dialog opens (a filled-in - form, and on iOS a protected document, is prepared first: seconds when - the document is long), - when the app is not in the foreground, and on iOS when the device cannot - print or the document's permissions forbid printing (Android has no API - to read them and prints anyway). Also rejects for a view showing several - sources (see +- Rejects with a `PdfMethodError` before `onLoad` (`not-loaded`), while + the view's print dialog is open or, on Android, when the app is not in + the foreground (`busy`), when the view closes or its `source` changes + before the dialog opens (`closed`; a filled-in form, and on iOS a + protected document, is prepared first: seconds when the document is + long), and on iOS when the device cannot print (`not-supported`) or the + document's permissions forbid printing (`not-permitted`; Android has no + API to read them and prints anyway). Also rejects for a view showing + several sources (`not-supported`, see [Several documents as one](sources-and-loading.md#several-documents-as-one)). On iPad the dialog opens as a popover from the view. - Printing a file the app already has needs no viewer: `onLoad`'s diff --git a/docs/guides/sources-and-loading.md b/docs/guides/sources-and-loading.md index 74ca861..bc9301f 100644 --- a/docs/guides/sources-and-loading.md +++ b/docs/guides/sources-and-loading.md @@ -133,7 +133,8 @@ smaller than its base64 form. shows none of them and `onError` reports `error.sourceIndex`. An empty list is an `invalid-source` error. `onLoadProgress` sums the downloads. - **Reading only:** forms cannot be filled in, and `print`, - `exportDocument`, `getFormFields` and `setFormFieldValues` reject. + `exportDocument`, `getFormFields` and `setFormFieldValues` reject with + `not-supported`. Printing and exporting need one file, and Android has no API that merges PDF files. - Links within a source lead to its pages, and "next page" / "previous diff --git a/docs/limitations.md b/docs/limitations.md index 33b1c46..fcdbd65 100644 --- a/docs/limitations.md +++ b/docs/limitations.md @@ -33,8 +33,8 @@ For an overview of what each platform supports, see the that page, copying ends the selection (as in a text field), dragging a handle to the edge does not scroll, and a handle dragged past the other end stops at the first character. -- **Outline (`getOutline`)**: on Android, encrypted documents reject (their - titles are encrypted). iOS (PDFKit) leaves PDF 1.1 named destinations +- **Outline (`getOutline`)**: on Android, encrypted documents reject with + `not-supported` (their titles are encrypted). iOS (PDFKit) leaves PDF 1.1 named destinations (`/Dest /name`) without a page. - **Metadata (`PdfDocument.getMetadata`)**: on Android, encrypted documents reject, like the outline. XMP metadata is not read, only the information diff --git a/example/describeError.ts b/example/describeError.ts new file mode 100644 index 0000000..2321d5b --- /dev/null +++ b/example/describeError.ts @@ -0,0 +1,8 @@ +import { PdfMethodError, PdfOpenError } from 'react-native-better-pdf'; + +/** A rejection for the event line: an open error's code, a method error's code and message. */ +export function describeError(error: unknown): string { + if (error instanceof PdfOpenError) return error.code; + if (error instanceof PdfMethodError) return `${error.code}: ${error.message}`; + return String(error); +} diff --git a/example/e2e/pdf-document.yaml b/example/e2e/pdf-document.yaml index 89c1126..92b01c2 100644 --- a/example/e2e/pdf-document.yaml +++ b/example/e2e/pdf-document.yaml @@ -30,7 +30,7 @@ appId: software.gmi.betterpdf.example - tapOn: "Thumbnails" - extendedWaitUntil: { visible: "thumbnails: 2 in .* ms", timeout: 10000 } - tapOn: "Close" -- extendedWaitUntil: { visible: "after close: The document is closed", timeout: 5000 } +- extendedWaitUntil: { visible: "after close: closed: The document is closed", timeout: 5000 } # Downloaded like a PdfView source. - tapOn: "metadata \\(remote\\)" @@ -61,7 +61,7 @@ appId: software.gmi.betterpdf.example - runFlow: when: { platform: Android } commands: - - assertVisible: "metadata failed: .*Encrypted documents are not supported.*" + - assertVisible: "metadata failed: not-supported: .*" - assertVisible: "sizes: 595x842 595x842" - assertVisible: "cover: 300x425" diff --git a/example/e2e/stress-close-during-work.yaml b/example/e2e/stress-close-during-work.yaml index da0cf18..254b4f7 100644 --- a/example/e2e/stress-close-during-work.yaml +++ b/example/e2e/stress-close-during-work.yaml @@ -56,7 +56,7 @@ appId: software.gmi.betterpdf.example - tapOn: "Thumbnails" - tapOn: "Close" # Below the bottom edge on Android's screen. - - scrollUntilVisible: { element: "after close: The document is closed", timeout: 30000 } + - scrollUntilVisible: { element: "after close: closed: The document is closed", timeout: 30000 } # Still alive. - tapOn: "Viewer" - runFlow: { file: _open-list-entry.yaml, env: { ENTRY: "Bundled sample.*" } } diff --git a/example/e2e/viewer-forms.yaml b/example/e2e/viewer-forms.yaml index 7958473..f1bf51f 100644 --- a/example/e2e/viewer-forms.yaml +++ b/example/e2e/viewer-forms.yaml @@ -27,7 +27,7 @@ env: # Rejected while the form is as loaded, when iOS exports a copy of the file: # an export into a directory used to delete it with everything in it. - tapOn: "Bad" -- extendedWaitUntil: { visible: "bad: .*read-only.* \\| .*cannot replace the displayed.* \\| .*export path is a directory.*; keep.txt kept", timeout: 10000 } +- extendedWaitUntil: { visible: "bad: not-permitted: .* \\| invalid-argument: .* \\| invalid-argument: .*; keep.txt kept", timeout: 10000 } # By touch: text is reported once, when editing ends; toggles at once. - tapOn: { point: "${NAME}" } @@ -85,7 +85,7 @@ env: visible: "fields 13: name=Ewa email=jan@example.com notes=First line/Second line agree=on plan=pro country=France colours=Blue id=A-1234 phone= address.city=Kraków postcode=00-950 date= signature=unsigned" timeout: 5000 - tapOn: "Bad" -- extendedWaitUntil: { visible: "bad: .*read-only.* \\| .*cannot replace the displayed.* \\| .*export path is a directory.*; keep.txt kept", timeout: 10000 } +- extendedWaitUntil: { visible: "bad: not-permitted: .* \\| invalid-argument: .* \\| invalid-argument: .*; keep.txt kept", timeout: 10000 } - tapOn: "Fields" - extendedWaitUntil: { visible: "fields 13: name=Ewa .*", timeout: 5000 } @@ -122,7 +122,7 @@ env: - tapOn: "Fill" - extendedWaitUntil: { visible: "fill: done", timeout: 5000 } - tapOn: "Bad" -- extendedWaitUntil: { visible: "bad: .*read-only.* \\| .*cannot replace the displayed.* \\| .*export path is a directory.* \\| .*in the cache of downloaded documents.*; keep.txt kept", timeout: 10000 } +- extendedWaitUntil: { visible: "bad: not-permitted: .* \\| invalid-argument: .* \\| invalid-argument: .* \\| invalid-argument: .*; keep.txt kept", timeout: 10000 } - tapOn: "‹ Back" - runFlow: { file: _open-list-entry.yaml, env: { ENTRY: "Remote form(, .*)?" } } - extendedWaitUntil: { visible: "1 / 2", timeout: 15000 } diff --git a/example/e2e/viewer-sources.yaml b/example/e2e/viewer-sources.yaml index 3277cec..0259356 100644 --- a/example/e2e/viewer-sources.yaml +++ b/example/e2e/viewer-sources.yaml @@ -60,7 +60,7 @@ appId: software.gmi.betterpdf.example # Viewing only: printing a set is rejected. - tapOn: "Print" -- extendedWaitUntil: { visible: "print: .*several sources.*", timeout: 5000 } +- extendedWaitUntil: { visible: "print: not-supported: Printing is not available with several sources", timeout: 5000 } - tapOn: "‹ Back" # All or nothing: a failing source fails the load and is named. diff --git a/example/screens/DocumentsScreen.tsx b/example/screens/DocumentsScreen.tsx index 3985726..b4535ae 100644 --- a/example/screens/DocumentsScreen.tsx +++ b/example/screens/DocumentsScreen.tsx @@ -2,12 +2,12 @@ import { useEffect, useRef, useState } from 'react'; import { Image, ScrollView, StyleSheet, Text, View } from 'react-native'; import { PdfDocument, - PdfOpenError, type PdfDocumentMetadata, type PdfOutlineItem, type PdfPageImage, type PdfSource, } from 'react-native-better-pdf'; +import { describeError } from '../describeError'; import { ensureFixture, FIXTURE_SERVER, type FixtureName } from '../documents'; import { Button, colors } from '../ui'; @@ -50,13 +50,6 @@ const TARGETS: Target[] = [ const MAX_THUMBNAILS = 12; -/** An open error's code, else the message's first line (on Android the stack follows). */ -function describeError(error: unknown): string { - if (error instanceof PdfOpenError) return error.code; - const message = error instanceof Error ? error.message : String(error); - return message.split('\n')[0] ?? message; -} - const date = (ms: number | undefined) => ms === undefined ? '-' : new Date(ms).toISOString(); diff --git a/example/screens/FormBar.tsx b/example/screens/FormBar.tsx index 9311129..1061ff8 100644 --- a/example/screens/FormBar.tsx +++ b/example/screens/FormBar.tsx @@ -2,6 +2,7 @@ import { Directory, File, Paths } from 'expo-file-system'; import { useState, type RefObject } from 'react'; import { StyleSheet, View } from 'react-native'; import type { PdfFormField, PdfViewRef } from 'react-native-better-pdf'; +import { describeError } from '../describeError'; import { Button } from '../ui'; /** @@ -34,8 +35,7 @@ export function FormBar({ }) { const [exported, setExported] = useState(null); const fail = (what: string) => (error: unknown) => - // Android's rejections carry the Java stack trace: first line only. - onEvent(`${what}: ${String(error).split('\n')[0]}`); + onEvent(`${what}: ${describeError(error)}`); return (