Skip to content

Stop exposing app data to the iOS Files app - #96531

Draft
NicolasBonet wants to merge 14 commits into
mainfrom
nicolas-ios-files-app-data-exposure
Draft

Stop exposing app data to the iOS Files app#96531
NicolasBonet wants to merge 14 commits into
mainfrom
nicolas-ios-files-app-data-exposure

Conversation

@NicolasBonet

@NicolasBonet NicolasBonet commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Explanation of Change

The iOS app enables UIFileSharingEnabled and LSSupportsOpeningDocumentsInPlace, which publishes the entire Documents/ directory to the iOS Files app. That exposed the Onyx database, attachment caches, and receipt images to the user (and any app with Files access), all of it user-deletable — deleting the OnyxDB breaks the app.

The Documents/ folder is also where downloaded PDFs/documents land, and the Files-app exposure is how users reach them (added in [Mobile-Expensify#2380728e0a]). Hiding the folder would make previously downloaded files unreachable (discussion), so this PR keeps the folder visible and instead moves every internal file out of Documents/, leaving it to hold only files the user expects to see: their downloads and queued receipt uploads.

  • Info.plist: unchanged — the file-sharing keys stay, so the "New Expensify" folder (and any previously downloaded file in it) remains visible in the Files app.
  • Download flow (src/libs/fileDownload/index.ios.ts): non-media downloads keep saving to Documents/. Temporary files move out of it: video downloads stage in Library/Caches before being saved to Photos, and the CSV-export flow writes its share-sheet hand-off file to Library/Caches instead of Documents/Expensify.
  • OnyxDBLibrary/Application Support via a react-native-nitro-sqlite patch ([patches/react-native-nitro-sqlite+9.6.0.patch]) that also migrates existing OnyxDB* files out of Documents/ at startup. The DB path is hardcoded to NSDocumentDirectory in the library's native +load, so this cannot be fixed at the app level; the change should be offered upstream so the patch can eventually be dropped.
  • Attachment cacheLibrary/Caches (re-downloadable, so purgeable is fine).
  • Debug state export (src/libs/ExportOnyxState/index.native.ts) and temporary share files (src/libs/localFileCreate/index.native.ts) → Library/Caches.
  • A new startup migration (src/libs/migrations/MoveFilesOutOfDocuments) deletes the old attachment cache (sources re-cache on demand) and removes a stale Onyx state dump if one was left in Documents/. Queued receipt uploads stay in Documents/ on purpose — they are user content, so they remain visible in the Files app.

Companion Mobile-Expensify PR (moves OldDot app data out of Documents/ while leaving user downloads in place):

MOBILE-EXPENSIFY: https://github.com/Expensify/Mobile-Expensify/pull/14023

Fixed Issues

$ https://github.com/Expensify/Expensify/issues/643611
PROPOSAL:

Tests

All file-visibility checks below refer to the iOS Files app under On My iPhone → New Expensify.

A. Fresh install (no migration)

  1. Install this build fresh and sign in. Verify the app boots with no JS console errors.
  2. Kill and relaunch the app. Verify you are still signed in and your data is intact (OnyxDB persists in its new location).
  3. Open the Files app folder. Verify it contains no OnyxDB* files, no attachments folder, and no other internal files.

B. Downloads (must stay user-visible)

  1. In a chat, download a PDF attachment. Verify the success message appears and the file shows up in the Files app folder.
  2. Download an image attachment. Verify it is saved to Photos, not the Files app folder.
  3. Download a video attachment. Verify it is saved to Photos and no temporary copy is left behind in the Files app folder.
  4. Export expenses as CSV: go to the Spend tab, long-press an expense (or use the ⋮ menu → Select multiple) and select a couple of expenses, then tap the "x selected" dropdown → ExportBasic export. Keep the selection small so the export downloads directly instead of being queued server-side. Verify the share sheet appears, "Save to Files" works, and no Expensify folder is left behind in the Files app folder.
  5. Download an attachment whose name is only digits (e.g. 2024.pdf). Relaunch the app and verify the file is still visible in the Files app folder (the OldDot migration must not sweep numeric-named user downloads).

C. Receipt uploads (user content, must stay visible)

  1. While offline, scan a receipt and create an expense. Verify the Files app folder shows the queued image under Receipts-Upload. Kill and reopen the app, go online, and verify the receipt uploads.

D. Attachment cache

  1. Open several image attachments in a chat so they get cached. Verify no attachments folder appears in the Files app folder.
  2. Go offline and reopen the same attachments. Verify they load from the cache.

E. Internal/debug files

  1. Settings → Troubleshoot → Export Onyx state. Verify the share sheet works and no dump file appears in the Files app folder.
  2. Settings → Security → Two-factor authentication → download the recovery codes. Verify the share/save flow works and no stray copy appears in the Files app folder.

F. Upgrade migration (NewDot)

  1. Install a build from main and sign in. Open image attachments in a chat (populates the attachment cache), download a PDF attachment, and export Onyx state (the old build leaves the dump in Documents). Install this build over it and verify:
    • You are still signed in and chats/expenses are intact (OnyxDB* including -wal/-shm migrated to Application Support).
    • The previously downloaded PDF is still visible in the Files app folder.
    • The Files app folder no longer contains OnyxDB* files, the attachments folder, or the Onyx state dump.
    • Attachments open normally (the cache was cleared and re-downloads on demand).
    • Relaunch once more and verify no errors — the migration must be a no-op on second run.

G. Upgrade migration (OldDot / HybridApp)

  1. On a main HybridApp build, use Expensify Classic (OldDot): sign in and capture a receipt in Classic. Upgrade to this build, open Classic again, and verify:
    • You are still signed in and account data loads (the JSON state caches were moved to Application Support/ExpensifyData).
    • The previously captured receipt still displays/uploads (stored absolute /Documents/ paths are re-rooted onto the new data directory).
    • The Files app folder root contains no *.json files, gpsLocations.txt, logPacketsQueue, <UUID>.jpg receipts, or hash-named image caches (e.g. 93244983.jpg) — while user downloads remain in place.
  2. Sign out of Expensify Classic and sign back in. Verify NewDot's session and data are unaffected (the Classic file-cache wipe only touches ExpensifyData).

H. Resilience (the original bug)

  1. In the Files app folder, delete everything that is visible. Relaunch the app and verify you are still signed in and nothing crashes — no internal file is user-deletable anymore.

Android sanity

  1. On Android, download a PDF and an image, open attachments, and scan a receipt. Verify behavior is unchanged (all path changes are iOS-scoped).

  2. Verify that no errors appear in the JS console throughout.

Offline tests

  1. While offline, scan a receipt and create an expense. Kill and reopen the app, go online. Verify the receipt uploads.
  2. While offline, reopen image attachments you viewed before. Verify they load from the cache.

QA Steps

Same as tests

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
Android: mWeb Chrome
iOS: Native
ScreenRecording_07-21-2026.15-23-31_1.MP4
iOS: mWeb Safari

@codecov

codecov Bot commented Jul 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Looks like you've decreased code coverage for some files. Please write tests to increase, or at least maintain, the existing level of code coverage. See our documentation here for how to interpret this table.

Files with missing lines Coverage Δ
src/libs/actions/Attachment/index.native.ts 75.47% <100.00%> (-1.89%) ⬇️
src/libs/migrateOnyx.ts 100.00% <100.00%> (ø)
...bs/migrations/MoveFilesOutOfDocuments/index.ios.ts 100.00% <100.00%> (ø)
src/libs/ExportOnyxState/index.native.ts 0.00% <0.00%> (ø)
src/libs/localFileCreate/index.native.ts 14.28% <0.00%> (ø)
...c/libs/migrations/MoveFilesOutOfDocuments/index.ts 0.00% <0.00%> (ø)
src/libs/fileDownload/index.ios.ts 3.29% <0.00%> (-0.08%) ⬇️
... and 7 files with indirect coverage changes

@github-actions

Copy link
Copy Markdown
Contributor

🚧 NicolasBonet has triggered a test Expensify/App build. You can view the workflow run here.

@github-actions

Copy link
Copy Markdown
Contributor

🚧 NicolasBonet has triggered a test Expensify/App build. You can view the workflow run here.

@github-actions

This comment has been minimized.

@github-actions

Copy link
Copy Markdown
Contributor

@NicolasBonet
NicolasBonet marked this pull request as ready for review July 21, 2026 20:25
@NicolasBonet
NicolasBonet requested review from a team as code owners July 21, 2026 20:25
@melvin-bot
melvin-bot Bot requested review from daledah and removed request for a team July 21, 2026 20:25
@melvin-bot

melvin-bot Bot commented Jul 21, 2026

Copy link
Copy Markdown

@daledah Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@melvin-bot
melvin-bot Bot requested review from heyjennahay and removed request for a team July 21, 2026 20:25

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 959f786962

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/libs/migrations/MoveFilesOutOfDocuments/index.ios.ts Outdated
Comment thread src/libs/migrations/MoveFilesOutOfDocuments/index.ios.ts Outdated
@github-actions

Copy link
Copy Markdown
Contributor

This PR adds a new Onyx.connectWithoutView call, so I've requested a review from the Onyx performance reviewers (@tgolen, @mountiny, @luacmartins) — a review from any one of them is enough. Please add a link in your PR description to the Slack discussion where the @frontend-performance team approved using connectWithoutView here.

@NicolasBonet
NicolasBonet marked this pull request as draft July 22, 2026 00:58
@NicolasBonet

Copy link
Copy Markdown
Contributor Author

This is WIP, will re-open when it's in a testable status

@NicolasBonet

NicolasBonet commented Jul 22, 2026

Copy link
Copy Markdown
Contributor Author

Heads up on a user-facing tradeoff in this PR that could use a product opinion — cc @Expensify/Product:

This PR hides the app's entire Documents folder from the iOS Files app, that includes any PDFs/documents users previously downloaded through the old flow and saved in the visible "New Expensify" folder. Those files are not migrated or surfaced anywhere — after this update they still exist on disk but become unreachable to the user.

The new download flow hands files off through the share sheet ("Save to Files"), so anything downloaded going forward is unaffected. The question is only about previously downloaded files.

Options:

  1. Accept the loss (files were re-downloadable copies of attachments, so nothing is unrecoverable) — current behavior of this PR.
  2. Have the migration surface old downloaded files once via the share sheet or move them somewhere user-accessible before hiding the folder.

Is option 1 acceptable, or should we handle existing downloads more gracefully? If 1, we should note it in the PR body/QA steps as expected behavior.

@trjExpensify

Copy link
Copy Markdown
Contributor

Woof, I don't want people to get angry thinking we've lost their curated list of downloads and force them to trawl through chats, reports, threads etc to re-download them.

What would it take to move them somewhere user accessible before hiding the folder?

@JmillsExpensify

Copy link
Copy Markdown
Contributor

I agree with Tom. I think it's dangerous to not give them someplace obvious to find them, at least for now. This is the number one thing that our users are counting on us for, keeping their digitized receipt data.

@NicolasBonet

Copy link
Copy Markdown
Contributor Author

Sounds good, let me work in a hybrid proposal and try to keep the folder while keeping onyx and similar data out of sight.

@NicolasBonet
NicolasBonet force-pushed the nicolas-ios-files-app-data-exposure branch from a71ba62 to af14bb0 Compare July 23, 2026 21:48
NicolasBonet and others added 2 commits July 24, 2026 12:31
Disable UIFileSharingEnabled and LSSupportsOpeningDocumentsInPlace and move
internal files out of the user-visible Documents directory:

- OnyxDB moves to Library/Application Support via a react-native-nitro-sqlite
  patch that also migrates existing database files
- The attachment cache moves to Library/Caches
- Queued receipt uploads move to Library/Application Support
- Non-media downloads now go through the share sheet ("Save to Files")
  instead of being written to Documents, with a startup migration cleaning
  up files left behind by older versions

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The global react-native-fs mock lacked the functions and directory constants
the MoveFilesOutOfDocuments migration uses, which crashed app-boot test
suites, and AttachmentTest still expected the attachment cache under the
documents directory. The migration also guards against synchronous throws so
a file-system error can never block startup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
NicolasBonet and others added 7 commits July 24, 2026 12:31
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… migration

The migration now copies queued receipts instead of moving them, rewrites the
receipt paths persisted in queued requests and transactions to the new upload
folder, and only deletes the originals once the rewrite has landed. Receipts
that fail to copy keep their only copy in the old directory, which is then
preserved, and their persisted paths are refreshed to the current container
path so the queued upload can still recover.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The receipts migration only rewrote transaction receipt.source, but odometer
images live in the same Receipts-Upload folder and are referenced from
transaction comments (odometerStartImage/odometerEndImage), mergeTransaction
entries, and the standalone odometer draft. Rewrite those references too so
in-progress odometer flows survive the move out of Documents.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Hiding the Documents directory entirely would make files users previously
downloaded through the app unreachable. Instead, the Info.plist file-sharing
keys stay and downloads keep saving to Documents, while everything internal
moves to directories the Files app never shows: temporary share-sheet and
video files go to Caches, the Onyx state dump goes to Caches and a stale
copy in Documents is removed by the startup migration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Queued receipt uploads are files the user expects to find in the app's
Files-app folder, so they stay in Documents alongside downloads. This
removes the receipt relocation and the persisted-path rewriting from the
startup migration, which now only cleans up internal leftovers: the old
attachment cache and a stale Onyx state dump.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@NicolasBonet
NicolasBonet force-pushed the nicolas-ios-files-app-data-exposure branch from 61adb7d to 9961ef4 Compare July 24, 2026 17:58
@trjExpensify

Copy link
Copy Markdown
Contributor

Sounds good, let me work in a hybrid proposal and try to keep the folder while keeping onyx and similar data out of sight.

Sounds good, thanks. 👍

NicolasBonet and others added 5 commits July 28, 2026 16:56
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-data-exposure

# Conflicts:
#	Mobile-Expensify
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants