Skip to content

[Home Page] Fix: Your spend section does not update after updating expense offline#93081

Open
adamgrzybowski wants to merge 35 commits into
Expensify:mainfrom
software-mansion-labs:@adamgrzybowski/fix-offline-your-spend
Open

[Home Page] Fix: Your spend section does not update after updating expense offline#93081
adamgrzybowski wants to merge 35 commits into
Expensify:mainfrom
software-mansion-labs:@adamgrzybowski/fix-offline-your-spend

Conversation

@adamgrzybowski

@adamgrzybowski adamgrzybowski commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Explanation of Change

  • The Home "Your spend" totals (Awaiting approval and Repaid in the last 30 days) come from cached search snapshots the server only refreshes while online, so most offline money-request actions left them stale until reconnect (only amount edits were patched).
  • This change optimistically patches the affected total when an action runs offline, and rolls it back on failure. Supported actions:
    • Submit / Retract / Unapprove (Awaiting approval): add / remove / re-add the report's reimbursable total.
    • Delete / Reject (Awaiting approval, also Repaid for delete): subtract the expense's amount.
    • Cancel payment (Repaid): subtract the report's total when a reimbursement is reversed.
    • Create is handled via submit (new reports stay Open until submitted); edit amount was already supported.
  • Patches are scoped to the user's own reimbursable expenses on paid group workspaces, skipped on currency mismatch, and reconciled by the next online refresh.

Fixed Issues

$ #91370
PROPOSAL:

Tests

Offline tests

QA Steps

Prerequisites: a paid group workspace (Team/Corporate) where you are a member, and a few of your own reimbursable expenses. Each suite is run while offline (disable your network connection), starting from Home where you note the current "Your spend" totals.

Suite 1 - Awaiting approval increases

  1. Submit an Open report with your reimbursable expenses, return to Home, and verify "Awaiting approval" increases by that report's total.
  2. Unapprove an approved report you own and verify "Awaiting approval" increases by that report's total.

Suite 2 - Awaiting approval decreases

  1. Retract a submitted report and verify "Awaiting approval" decreases by that report's total.
  2. Reject a single submitted reimbursable expense and verify "Awaiting approval" decreases by that expense's amount.
  3. Delete a single submitted reimbursable expense and verify "Awaiting approval" decreases by that expense's amount.

Suite 3 - Repaid (last 30 days)

  1. Cancel a payment on a report you reimbursed within the last 30 days and verify "Repaid" decreases by that report's total.

Suite 4 - Rollback and reconciliation

  1. Perform any covered action offline and force the request to fail, then verify the "Your spend" total rolls back to its previous value.
  2. Reconnect and verify all totals reconcile with the server (no drift / no double counting).

Suite 5 - Negative checks (totals must not change)

  1. Make a currency-only edit to an expense and verify no "Your spend" total changes.
  2. Perform a covered action on a non-reimbursable expense and verify no total changes.
  3. Perform a covered action on a personal/IOU (non paid-group) expense and verify no total changes.
  • Verify that no errors appear in the JS console

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 any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick)
    • 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 shown in the product is localized by adding it to src/languages/* files and using the translation method
    • I verified all numbers, amounts, dates and phone numbers shown in the product are using the localization methods
    • 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)
    • I verified proper file naming conventions were followed for any new files or renamed files. All non-platform specific files are named after what they export and are not named "index.js". All platform-specific files are named for the platform the code supports as outlined in the README.
    • I verified the JSDocs style guidelines (in STYLE.md) were followed
  • 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)
  • I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests)
  • I verified any variables that can be defined as constants (ie. in CONST.ts or at the top of the file that uses the constant) are defined as such
  • I verified that if a function's arguments changed that all usages have also been updated correctly
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • 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.
  • If a new page is added, I verified it's using the ScrollView component to make it scrollable when more elements are added to the page.
  • 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
iOS: mWeb Safari
MacOS: Chrome / Safari
Screen.Recording.2026-06-09.at.16.05.31.mov

Patch awaiting-approval and repaid snapshot search.total aggregates optimistically when money request amounts change, since Home cannot refetch those totals while offline.
Import Your spend query builders directly from @libs/YourSpendQueryUtils
instead of a page-local re-export barrel, giving a single source of truth.
@adamgrzybowski adamgrzybowski requested review from a team as code owners June 9, 2026 14:47
@melvin-bot melvin-bot Bot requested review from ZhenjaHorbach and joekaufmanexpensify and removed request for a team June 9, 2026 14:47
@melvin-bot

melvin-bot Bot commented Jun 9, 2026

Copy link
Copy Markdown

@ZhenjaHorbach 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 a team and grgia and removed request for a team June 9, 2026 14:47
@melvin-bot

melvin-bot Bot commented Jun 9, 2026

Copy link
Copy Markdown

@grgia 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]

@codecov

codecov Bot commented Jun 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ Changes either increased or maintained existing code coverage, great job!

Files with missing lines Coverage Δ
...Item/MoneyRequestReportPreview/PayActionButton.tsx 91.54% <100.00%> (+0.12%) ⬆️
...c/components/ReportActionItem/MoneyRequestView.tsx 73.72% <100.00%> (+0.06%) ⬆️
...earch/SearchList/ListItem/ReportListItemHeader.tsx 91.78% <100.00%> (+0.11%) ⬆️
src/components/SettlementButton/index.tsx 82.19% <100.00%> (+0.06%) ⬆️
src/hooks/useDeleteTransactions.ts 55.00% <100.00%> (+0.32%) ⬆️
src/hooks/useSearchBulkActions.ts 53.89% <100.00%> (+0.04%) ⬆️
src/hooks/useSelectionModePayment.ts 62.50% <100.00%> (+0.36%) ⬆️
src/hooks/useYourSpendPatchData.ts 100.00% <100.00%> (ø)
src/libs/IOUAmountSubmission.ts 75.16% <ø> (ø)
src/libs/PaymentUtils.ts 79.79% <ø> (ø)
... and 34 more
... and 29 files with indirect coverage changes

Comment thread src/libs/actions/IOU/UpdateMoneyRequest.ts Outdated
Comment thread src/libs/actions/IOU/YourSpendSnapshotUpdate.ts
@grgia

grgia commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

@adamgrzybowski lint

@joekaufmanexpensify joekaufmanexpensify left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Good for product

Replace unsafe type assertions with a typed createRandomPolicy factory and
typed getSnapshotKey/buildSnapshotSearchResults helpers.
@adamgrzybowski

Copy link
Copy Markdown
Contributor Author

@grgia fixed

@ZhenjaHorbach

Copy link
Copy Markdown
Contributor

Looks like changes don't work if we reject submitted expenses
In this case, we also need to update Your spend section

2026-06-11.14.48.04.mov

@adamgrzybowski

Copy link
Copy Markdown
Contributor Author

@ZhenjaHorbach Yeah, the question is how big the scope should be for this PR and which actions should be supported.

This issue is also mentioned here #93081 (comment)

@ZhenjaHorbach

Copy link
Copy Markdown
Contributor

@ZhenjaHorbach Yeah, the question is how big the scope should be for this PR and which actions should be supported.

This issue is also mentioned here #93081 (comment)

I believe all actions should be supported, and the amount should be the same as after the response from BE

@adamgrzybowski

Copy link
Copy Markdown
Contributor Author

@ZhenjaHorbach do you know exactly which actions?

  • create
  • delete
  • edit amount

Something else?

@ZhenjaHorbach

Copy link
Copy Markdown
Contributor

@ZhenjaHorbach do you know exactly which actions?

  • create
  • delete
  • edit amount

Something else?

I suppose in the context of this flow, at least retract, unapprove, reject

Comment thread src/libs/actions/IOU/YourSpendSnapshotUpdate.ts Outdated
@github-actions

github-actions Bot commented Jul 3, 2026

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.

- Drop removed transactions from snapshot.data so the Search page stays in sync offline
- Mirror only each snapshot's search aggregates to keep memory bounded
- Align 30-day boundary checks with the exclusive date> filter semantics
Comment on lines +47 to +54
let allPolicies: OnyxCollection<Policy> = {};
Onyx.connectWithoutView({
key: ONYXKEYS.COLLECTION.POLICY,
waitForCollectionCallback: true,
callback: (value) => {
allPolicies = value ?? {};
},
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think that instead of adding this one here again, maybe we could reuse what is already in the ReportUtils - getPolicy and getAllPolicies, so we do not add another subscription to policies collection in another file. Curious what @tgolen @luacmartins thinks

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Well, ReportUtils is being refactored to remove those eventually, so even that's not a good pattern to follow. Why can't these just be passed from the view component?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yeah that is fair, it would just create sort of a singleton pattern that I am not sure if we want.

Why can't these just be passed from the view component?

@adamgrzybowski

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

These functions run in the IOU action layer (delete/submit/reject/pay/split), not in a component. They are triggered from expense screens when Home isn't always mounted

I am looking for an alternative solution, but so far I don't see a solution without tradeoffs

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Is avoiding the connectWithoutView a hard requirement?
cc: @tgolen @mountiny

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It's a pretty hard requirement, yes.

These functions run in the IOU action layer (delete/submit/reject/pay/split), not in a component.

But they are called by a component somewhere up the chain, aren't they? Otherwise, what calls these functions?

They are triggered from expense screens when Home isn't always mounted

How can they be triggered from a screen when Home isn't mounted?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Whichever component is triggering the actions, there you can subscribe to the policies and thread them through the functions @adamgrzybowski

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

How can they be triggered from a screen when Home isn't mounted?

What I meant here is that these actions are triggered by many different components (5 hooks and 8 components), so it's not as straightforward as passing these props from the home component.

However, I understand that we want to get rid of the connectWithoutView.

I will create a commit with the change, so we can judge if this approach is good enough by looking at the diff.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Yeah that is expected, we need to pull the data from the views

@tgolen tgolen left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I have a few general concerns about this PR. It's a massive fix for the issue that was originally reported.

  • Are we sure this is the best fix?
  • Are we sure that it needs to be fixed and we can't live with the original problem?

Comment on lines +47 to +54
let allPolicies: OnyxCollection<Policy> = {};
Onyx.connectWithoutView({
key: ONYXKEYS.COLLECTION.POLICY,
waitForCollectionCallback: true,
callback: (value) => {
allPolicies = value ?? {};
},
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Well, ReportUtils is being refactored to remove those eventually, so even that's not a good pattern to follow. Why can't these just be passed from the view component?

Comment thread src/libs/actions/IOU/DeleteMoneyRequest.ts
Comment thread src/libs/actions/IOU/YourSpendSnapshotUpdate.ts
Comment thread src/libs/actions/IOU/SplitTransactionUpdate.ts Outdated

@grgia grgia left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

comments

Split updates now diff reimbursable portions only, and toggling an
expense's reimbursable flag offline patches the Your spend totals.
Cover getYourSpendSnapshotReimbursableUpdates in both directions and the
getUpdateMoneyRequestParams branch that removes an expense toggled non-reimbursable.
@adamgrzybowski

Copy link
Copy Markdown
Contributor Author

I fixed problems mentioned by @grgia, and now I am looking for alternative solutions that don't require connectWithoutView

@adamgrzybowski

Copy link
Copy Markdown
Contributor Author

I removed the connectWithoutView from this PR

@mountiny mountiny left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Overall lets make sure every new or update utils method or action has appropriate test here

Comment thread src/libs/actions/IOU/YourSpendSnapshotUpdate.ts
Comment thread src/libs/YourSpendQueryUtils.ts
Comment thread Mobile-Expensify
Comment thread src/pages/home/YourSpendSection/useYourSpendData.ts
Comment thread src/libs/actions/IOU/ReportWorkflow.ts
/** Supplies the paid-group policies and "Your spend" snapshot aggregates the offline snapshot-patch builders need. */
function useYourSpendPatchData(): YourSpendPatchData {
const {accountID} = useCurrentUserPersonalDetails();
const [paidPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: selectPaidGroupPolicies});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Scrolling large Search results and any policy-collection update now do extra per-row Onyx work, which bites on accounts with many workspaces.

could we mount useYourSpendPatchData once at the Search page level and pass it to rows via the existing SearchContext?

or narrow selectPaidGroupPolicies to the fields the builders actually use ({id, type, outputCurrency})

@grgia grgia left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

comments

Comment on lines +88 to +95
const currentTotal = search.total ?? 0;
const currentCount = search.count ?? 0;

if (currentCurrency && currentCurrency !== currency) {
return {optimisticData: [], successData: [], failureData: []};
}

const updatedTotal = currentTotal + diff;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Deleting or rejecting several expenses at once while offline leaves the Home Awaiting approval total wrong (only one expense subtracted) until reconnect.

Could we do a snapshot update per batch instead of one per transaction?

@grgia grgia left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

.

…offline-your-spend

# Conflicts:
#	src/components/MoneyReportHeaderActions/MoneyReportHeaderSecondaryActions.tsx
#	src/components/MoneyReportHeaderPrimaryAction/useConfirmApproval.ts
#	src/components/MoneyRequestHeaderSecondaryActions.tsx
#	src/components/ReportActionItem/MoneyRequestReportPreview/ApproveActionButton.tsx
#	src/components/ReportActionItem/MoneyRequestReportPreview/PayActionButton.tsx
#	src/components/ReportActionItem/MoneyRequestReportPreview/SubmitActionButton.tsx
#	src/components/ReportActionItem/MoneyRequestView.tsx
#	src/components/Search/SearchList/ListItem/ExpenseReportListItem.tsx
#	src/components/Search/SearchList/ListItem/ReportListItemHeader.tsx
#	src/components/Search/SearchList/ListItem/TransactionListItem/index.tsx
#	src/hooks/useDeleteTransactions.ts
#	src/hooks/useHoldMenuSubmit.ts
#	src/hooks/useLifecycleActions.tsx
#	src/hooks/useSearchBulkActions.ts
#	src/hooks/useSelectionModePayment.ts
#	src/hooks/useTransactionInlineEdit.ts
#	src/libs/IOUAmountSubmission.ts
#	src/libs/PaymentUtils.ts
#	src/libs/actions/IOU/DeleteMoneyRequest.ts
#	src/libs/actions/IOU/PayMoneyRequest.ts
#	src/libs/actions/IOU/RejectMoneyRequest.ts
#	src/libs/actions/IOU/ReportWorkflow.ts
#	src/libs/actions/IOU/SplitTransactionUpdate.ts
#	src/libs/actions/IOU/TrackExpense.ts
#	src/libs/actions/IOU/UpdateMoneyRequest.ts
#	src/libs/actions/Search.ts
#	src/pages/DynamicReportDetailsPage.tsx
#	src/pages/RejectExpenseReportPage.tsx
#	src/pages/home/YourSpendSection/useYourSpendData.ts
#	src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx
#	src/pages/iou/SplitExpensePage.tsx
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.

6 participants