fix: Invoice - stop infinite loading when viewing an offline-created invoice online - #97112
Conversation
…sify#96925) Offline-created invoices never wrote RAM_ONLY_REPORT_LOADING_STATE, so on reconnect the invoice report and its optimistic invoice room stayed pinned at isLoadingInitialReportActions and showed an infinite loading skeleton. Mirror the expense flow in MoneyRequestBuilder by stamping hasOnceLoadedReportActions: true for both reports in optimisticData. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
|
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a200158223
ℹ️ 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".
| key: `${ONYXKEYS.COLLECTION.RAM_ONLY_REPORT_LOADING_STATE}${chat.report?.reportID}`, | ||
| value: { | ||
| hasOnceLoadedReportActions: true, |
There was a problem hiding this comment.
Preserve invoice readiness across app restarts
If the app or browser restarts after this offline write—particularly before the invalid SendInvoice request is rejected—this value is discarded because src/setup/index.ts registers this collection as RAM-only, and prepareRequest() strips optimisticData from the persisted request. The report, actions, and optimistic metadata remain persisted, so after the queued request fails, ReportFetchHandler still skips openReport() for the optimistic invoice room and re-arms isLoadingInitialReportActions, producing the same infinite skeleton. Reconstruct readiness from the persisted optimistic report/actions on startup, or otherwise make this state survive a restart, rather than relying solely on this RAM-only merge.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good catch. The creation-time stamp lives in a RAM-only key, so a restart between the offline create and reconnect would drop it and the skeleton would come back.
Fixed by also settling the state on the read side: when ReportFetchHandler intentionally skips openReport for an optimistic chat report, it now reconstructs the readiness stamp (markLocalReportActionsAsLoaded) from the persisted optimistic data, so it no longer depends on the RAM-only value surviving a restart. The creation-time stamp stays for parity with the expense flow and to cover the first session immediately. Added a unit test for the reconstruction path.
…estarts The creation-time hasOnceLoadedReportActions stamp lives in a RAM-only key, so an app restart between the offline create and reconnect drops it, and the queued SendInvoice failure would leave the invoice room pinned on the loading skeleton again. When ReportFetchHandler intentionally skips openReport for an optimistic chat report, settle the loading state from the persisted optimistic data instead of relying solely on the RAM-only stamp. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Explanation of Change
When an invoice is created offline,
buildOnyxDataForInvoice(SendInvoice.ts) writes the invoice report and the new invoice room as optimistic reports (isOptimisticReport: true) but — unlike every other money-request creation flow — it never stampsRAM_ONLY_REPORT_LOADING_STATE.hasOnceLoadedReportActions: true.The invoice room is a
CHAT(chatTypeINVOICE), andReportFetchHandlerdeliberately never callsopenReportfor an optimistic chat report, so nothing ever writes that flag. Worse, the fetch handler keeps re-armingisLoadingInitialReportActionsback totruewhilehasOnceLoadedReportActionsisfalse. So once the user reconnects and opens the invoice, the report is pinned in a loading state and shows an infinite skeleton. Because the create was rejected server-side (invalid company website), the report exists only in local Onyx, which is why clearing the cache makes it disappear.This brings
buildOnyxDataForInvoiceto parity with the expense flow inMoneyRequestBuilder: stamphasOnceLoadedReportActions: trueinoptimisticDatafor both the invoice report and the newly-created invoice room. The optimistic data is already the complete local truth for these reports, so "loaded" is correct from creation. Once the wait ends, the invoice room renders its locally-written created action, whose existingOfflineWithFeedbacksurfaces theerrorFields.createChatred-brick-road error thatfailureDataalready writes.isOptimisticReportandfailureDataare intentionally left untouched soclearCreateChatError's dismiss-and-delete behavior is preserved.Fixed Issues
$ #96925
PROPOSAL: #96925 (comment)
Tests
www.none.com).Offline tests
Same as the Tests steps above — the repro requires creating the invoice while offline, then going online.
QA Steps
www.none.com).PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectionAvatar, 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
96925-fix.mp4