Save unsent receipts to the device gallery on sign-out#94973
Conversation
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ut save getPendingReceiptRequests() missed pending receipts in two ways, both of which let Onyx.clear() delete a local receipt on sign-out without saving it: - REPLACE_RECEIPT queues the raw File object, whose local path lives under `uri` (and name under `name`), not `source`/`localSource`. It was allow-listed but could never match, so every offline receipt-replace was silently dropped. Fall back to `uri` for the path and `name` for the filename. - Other receipt-bearing commands (SEND_MONEY_ELSEWHERE, SEND_MONEY_WITH_WALLET, SHARE_TRACKED_EXPENSE, CATEGORIZE_TRACKED_EXPENSE, ADD_TRACKED_EXPENSE_TO_POLICY) were absent from the allow-list. Add them. Also collapse the repeated string-property guards into a single isRecord-based getString helper. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Hey, I noticed you changed If you want to automatically generate translations for other locales, an Expensify employee will have to:
Alternatively, if you are an external contributor, you can run the translation script locally with your own OpenAI API key. To learn more, try running: npx ts-node ./scripts/generateTranslations.ts --helpTypically, you'd want to translate only what you changed by running |
Codecov Report✅ Changes either increased or maintained existing code coverage, great job!
|
|
I applied the coding-standards review fixes on top of the cherry-picked commits. One thing I wanted confirm - when the user is offline with a pending receipt and taps Sign Out, we show two back-to-back modals - first the pre-existing "you'll lose your offline data" warning, then the new "you have 1 receipt in the queue, save it to the gallery?" prompt. Cancelling either aborts sign-out. Since the save-receipts feature specifically exists for the offline case, both modals will almost always fire together -not sure this is intended. Maybe we could merge the modals into one. Adding videos below: Android: ReceiptsModal-Android.mp4iOS: ReceiptsModal-iOS.mp4 |
|
@eVoloshchak 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] |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 37c834e476
ℹ️ About Codex in GitHub
Codex has been enabled to automatically 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 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| // `Onyx.clear` reinitializes the Onyx instance with initial values so use `Onyx.merge` instead of `Onyx.set` | ||
| Onyx.merge(ONYXKEYS.SESSION, {errors: getMicroSecondOnyxErrorWithMessage(errorMessage)}); | ||
| }); | ||
| return saveUnsentReceiptsBeforeClear().then(() => |
There was a problem hiding this comment.
Avoid permission-gated saves during forced sign-out
redirectToSignIn is used for invalid/expired-session redirects as well as the Settings button, but this unconditional save runs before Onyx.clear() for all of them. On a forced sign-out with a queued receipt and no Photos/storage permission, the native save path can show OS permission UI (Android requests it in hasGalleryWritePermission.android.ts, iOS prompts from CameraRoll.saveAsset) and the clear/redirect waits on that promise, so the supposedly silent forced sign-out can be delayed or appear blocked until the user responds. Gate permission-prompting saves to the manual flow or use a non-prompting check for forced redirects.
Useful? React with 👍 / 👎.
|
@eVoloshchak please have a look on the PR. I addressed the codex review comments |
|
Oh I think we should only show the 1st modal really? and the logic should run for it, I didn't know we have this already. |
|
So one modal to rule them all? 😄 We can combine it into something like that:
wdyt? |
Reviewer Checklist
Screenshots/VideosAndroid: HybridAppscreen-20260706-215600-1783367710526.mp4Android: mWeb ChromeScreen.Recording.2026-07-06.at.23.18.09.moviOS: HybridAppScreen.Recording.2026-07-09.at.13.26.29.moviOS: mWeb SafariScreen.Recording.2026-07-06.at.22.06.44.movMacOS: Chrome / SafariScreen.Recording.2026-07-06.at.22.05.25.mov |
| } | ||
|
|
||
| return PermissionsAndroid.requestMultiple([PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE, PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE]).then( | ||
| (status) => status['android.permission.READ_EXTERNAL_STORAGE'] === 'granted' && status['android.permission.WRITE_EXTERNAL_STORAGE'] === 'granted', |
There was a problem hiding this comment.
| (status) => status['android.permission.READ_EXTERNAL_STORAGE'] === 'granted' && status['android.permission.WRITE_EXTERNAL_STORAGE'] === 'granted', | |
| (status) => status[PermissionsAndroid.PERMISSIONS.READ_EXTERNAL_STORAGE] === PermissionsAndroid.RESULTS.GRANTED && status[PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE] === PermissionsAndroid.RESULTS.GRANTED, |
|
Looks and works well on web/Android! |
|
thanks! I added the requested changes I cannot run the adhoc - someone internal from Expensify should be able to do it. @mountiny could you help and run the adhoc build for it, please? |
|
🚧 mountiny has triggered a test Expensify/App build. You can view the workflow run here. |
This comment has been minimized.
This comment has been minimized.
mountiny
left a comment
There was a problem hiding this comment.
Just one comment, but not sure how to go about that, looks good to me
| const RECEIPT_BEARING_COMMANDS = new Set<string>([ | ||
| WRITE_COMMANDS.REQUEST_MONEY, | ||
| WRITE_COMMANDS.TRACK_EXPENSE, | ||
| WRITE_COMMANDS.SPLIT_BILL, | ||
| WRITE_COMMANDS.SPLIT_BILL_AND_OPEN_REPORT, | ||
| WRITE_COMMANDS.START_SPLIT_BILL, | ||
| WRITE_COMMANDS.REPLACE_RECEIPT, | ||
| WRITE_COMMANDS.SEND_MONEY_ELSEWHERE, | ||
| WRITE_COMMANDS.SEND_MONEY_WITH_WALLET, | ||
| WRITE_COMMANDS.SHARE_TRACKED_EXPENSE, | ||
| WRITE_COMMANDS.CATEGORIZE_TRACKED_EXPENSE, | ||
| WRITE_COMMANDS.ADD_TRACKED_EXPENSE_TO_POLICY, | ||
| ]); |
There was a problem hiding this comment.
I am not sure what is a better solution but my worry with this is that we could add a new command that will carry a request and people will forget to add it here. Either way, just raising that as I do not have better idea how to avoid it
|
✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release. |
|
🚧 mountiny has triggered a test Expensify/App build. You can view the workflow run here. |
|
🧪🧪 Use the links below to test this adhoc build on Android, iOS, and Web. Happy testing! 🧪🧪
|
|
🚀 Deployed to staging by https://github.com/mountiny in version: 9.4.33-0 🚀
|
📝 Help site review — docs update neededYes, this PR introduces new user-facing behavior worth documenting: on iOS and Android, signing out while receipts are still uploading now saves those receipts to the device's photos, and manual sign-out shows a Save your receipts? confirmation prompt with a Save and sign out button. Nothing under What I did: Rather than create a whole new article for this edge case (one-workflow-per-article rule), I added a targeted FAQ entry to the existing New Expensify Create an Expense article, which already documents mobile SmartScan. All button/feature labels (Settings, Save your receipts?, Save and sign out, Sign out) were verified against the copy added in this PR and the current UI, per Draft help site PR: #95844 (labeled FAQ entry added
@rinej, please review the linked help site PR and confirm it reflects the current behavior. Then mark the linked help site PR |

Explanation of Change
It is the PR based on the original -> #94327
On iOS and Android, if you sign out while a receipt is still uploading, the app now saves that receipt image to your device gallery before it clears storage. That way you can add it again later instead of losing it.
Manual sign-out from Settings shows a short confirmation prompt. Forced sign-outs (expired or invalid session) save silently without a prompt. If saving fails for any reason, sign-out still completes normally.
Fixed Issues
$ #92139
PROPOSAL:
Tests
Tests
Unit tests (all passing):
Offline tests
QA Steps
iOS Native and Android Native only
-Repeat step 1. Deny photo-library permission when prompted (or pre-deny it in Settings). Tap Sign Out and confirm. - Verify sign-out still completes even though the gallery save fails.
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectiontoggleReportand notonIconClick)Avatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari