Skip to content

[Due for payment 2026-10-07] [$175] Spend - Total spend footer flickers to a loading skeleton after Select all #102339

Closed

Description

@applause-bot

If you haven’t already, check out our contributing guidelines for onboarding. To join our Slack channel, fill out this form.


Version Number: 9.4.94-0
Reproducible in staging?: Yes
Reproducible in production?: Yes
If this was caught during regression testing, add the test name, ID and link from BrowserStack: #98411
Email or phone of affected tester (no customers): ibellicotest+2766@gmail.com
Issue reported by: Applause Internal Team
Bug source: Exploratory - Significant User Experience Deterioration
Device used: Motorola MotoG60 / Android 12 (Hybrid app)
App Component: Search

Action Performed:

Prerequisite: More than 50 unreported expenses created, so Spend > Expenses has more than one page.

  1. Open the Expensify app while online.
  2. Navigate to "Spend" > "Expenses".
  3. With nothing selected, tap the header checkbox and select "Select all".
  4. Watch the total spend footer.

Expected Result:

The footer shows the correct total expenses and total spend once, and stays on it.

Actual Result:

The footer flickers. It briefly shows the correct totals, then switches to the loading skeleton, then shows the correct totals again.

Workaround:

Unknown

Platforms:

  • Android: App
  • Android: mWeb Chrome
  • iOS: App
  • iOS: mWeb Safari
  • iOS: mWeb Chrome
  • Windows: Chrome
  • MacOS: Chrome Safari

Screenshots/Videos

Flicker while online:

Screen.Recording.2026-09-28.at.2.04.30.PM.mov

View all open jobs on GitHub

Applause Internal Information
Exported by: Nathan Lemma
Bug ID: 7269567

Upwork Automation - Do Not Edit
Issue OwnerCurrent Issue Owner: @mallenexpensify

Activity

  1. added
    BugSomething is broken. Auto assigns a BugZero manager.
    on Sep 25, 2026
  2. applause-bot commented on Sep 25, 2026

    @applause-bot
    Author

    While Applause doesn't embed attachments for this bug, here are the links for quick view:
    https://platform.applause.com/services/links/v1/external/9d58fdbd1f8ed42e187cba4f5dabc0182f0745cc7941fb9661f17f4c41817dad

  3. MelvinBot commented on Sep 25, 2026

    @MelvinBot
    Contributor

    Proposal

    What is the root cause of that problem?

    When all matching items are selected, the footer only shows once the server returns a total count. Offline, that request is skipped until reconnect, so the count stays empty and the footer returns null.

    "Select all on this page" leaves areAllMatchingItemsSelected false. The first clause then shows the footer with a total computed from the loaded rows.

    Code references
    • SearchSelectionFooter.tsx:351: shouldShowFooter = (!areAllMatchingItemsSelected && selectedTransactionsKeys.length > 0) || (shouldAllowFooterTotals && !!metadata?.count). With "Select all" offline, both clauses are false, so the footer returns null at L611-L613.
    • Search/index.tsx:455-459: the search that calculates totals returns early while offline. It runs again on reconnect, which is why the footer appears once you're back online.
    • Search.ts:786-804: the normal Spend > Expenses search clears count/total in the cached snapshot, so no count is cached offline.
    • "Select all" already fills selectedTransactions with every loaded row, so local data is available. Only the visibility check blocks it.
    • SearchSelectionFooter.tsx:294: shouldUseClientTotal = !metadataCount || ... already falls back to the local total when the count is missing.
    • SearchBulkActionsButton.tsx:185-188 already handles this case. Offline, it falls back to the count of the loaded selected items. The footer doesn't.

    Origin: Search: show expense count and total when "Select all matching items" is active made this case depend on server totals, with no offline fallback. Before that, Hide total footer when Select All Matching Items is active hid the footer for all-matching selections on purpose.

    What changes do you think we should make in order to solve the problem?

    In SearchSelectionFooter.tsx, let the first clause of shouldShowFooter also pass when the user is offline. The rule becomes: if all matching items are selected and the app is offline, show the footer from the loaded selection, like SearchBulkActionsButton does. footerData already computes the count and total from the loaded rows (minus excluded ones) when no server count exists. Online behavior stays the same.

    Also skip the loading skeleton while offline. Otherwise, a totals request that was in flight when the connection dropped can leave the footer stuck loading.

    Add a unit test in tests/unit/Search/SearchSelectionFooterTest.tsx. It should set offline, set areAllMatchingItemsSelected: true, and provide loaded selections with no metadata.count. It should then check that the footer renders the loaded count and total.

    What alternative solutions did you explore? (Optional)

    Copy the bulk button's exact condition: show the local fallback only when all matching items are selected, no server count exists, and the app is offline or the search has errors. This is stricter and also covers search errors.

    Product note: Offline, the footer shows figures for loaded rows only (for example, 50 expenses), not every matching expense. The offline footer already greys out its values, and the server totals replace them on reconnect.

    Related issue: Inconsistency when moving all unreported expenses offline during first & second bulk select is in the same area (selecting all matching items offline), but uses a different code path.


    Next Steps for Contributor+ team:
    To accept: @MelvinBot implement [this](https://github.com/Expensify/App/issues/102339) to create a draft PR.
    To refine: @MelvinBot <your feedback>
    To reject: Explain why you are rejecting Melvin's proposal.


    view run

  4. added
    ExternalAdded to denote the issue can be worked on by a contributor
    on Sep 25, 2026
  5. melvin-bot commented on Sep 25, 2026

    @melvin-bot

    Triggered auto assignment to Contributor-plus team member for initial proposal review - @mkhutornyi (External)

  6. changed the title [-]Android - Spend - Total spend footer not displayed after selecting all expenses offline[/-] [+][$175] Android - Spend - Total spend footer not displayed after selecting all expenses offline[/+] on Sep 25, 2026
  7. melvin-bot commented on Sep 25, 2026

    @melvin-bot
  8. ashusatyarthi-wq commented on Sep 25, 2026

    @ashusatyarthi-wq

    Proposal

    What is the root cause of that problem?

    This is a regression introduced in PR #98411 ("Support 'Select all matching' when moving expenses to a report").

    In src/components/Search/SearchSelectionFooter.tsx (line 351), shouldShowFooter is guarded by:

    const shouldShowFooter = (!areAllMatchingItemsSelected && selectedTransactionsKeys.length > 0) || (shouldAllowFooterTotals && !!metadata?.count);
    1. When a user selects "Select all on this page", areAllMatchingItemsSelected remains false and selectedTransactionsKeys.length > 0 is true. The first clause evaluates to true, displaying the client-calculated total computed by shouldUseClientTotal (line 294).
    2. When the user selects "Select All" (matching items), areAllMatchingItemsSelected becomes true. This causes !areAllMatchingItemsSelected to evaluate to false, forcing the component to fall back to the second clause: (shouldAllowFooterTotals && !!metadata?.count).
    3. In offline mode, the query to fetch search metadata totals cannot reach the server, leaving metadata?.count undefined. Consequently, shouldShowFooter evaluates to false and the component returns null at line 612, hiding the footer completely despite items being selected locally.

    What changes do you think we should make in order to solve the problem?

    In src/components/Search/SearchSelectionFooter.tsx, allow shouldShowFooter to remain active when selecting all items offline by falling back to the client-side calculated totals:

    const shouldShowFooter = (!areAllMatchingItemsSelected && selectedTransactionsKeys.length > 0) || (shouldAllowFooterTotals && (!!metadata?.count || (isOffline && areAllMatchingItemsSelected)));

    Why this is completely safe:

    • When offline, shouldUseClientTotal (line 294: !metadataCount || hasPartialSelection) already properly resolves to true because metadataCount is undefined.
    • At line 542, footerData correctly returns {count: selectedExpenseCount, total, currency} using the loaded offline expenses.
    • When back online, metadata?.count is retrieved normally from the server, preserving the standard online behavior with zero regressions.

    What alternative solutions did you explore? (Optional)

    We could also set an optimistic fallback on metadata.count inside SearchContext when selecting all offline, but gating it in SearchSelectionFooter.tsx is cleaner and directly aligns with the component's existing client-side totals calculation without introducing side-effects into the search state.

    Test Plan:

    1. Open Expensify and navigate to "Spend" > "Expenses".
    2. Disconnect internet connection (go offline).
    3. Select an expense, open the header menu, and choose "Select all on this page" -> Verify footer displays with total expenses and total spend.
    4. Clear selection, then select an expense and choose "Select All" -> Verify footer displays seamlessly with the offline expense totals.
    5. Reconnect internet -> Verify search metadata updates without flicker or layout shifts.
  9. melvin-bot commented on Sep 25, 2026

    @melvin-bot

    📣 @ashusatyarthi-wq! 📣
    Hey, it seems we don’t have your contributor details yet! You'll only have to do this once, and this is how we'll hire you on Upwork.
    Please follow these steps:

    1. Make sure you've read and understood the contributing guidelines.
    2. Get the email address used to login to your Expensify account. If you don't already have an Expensify account, create one here. If you have multiple accounts (e.g. one for testing), please use your main account email.
    3. Get the link to your Upwork profile. It's necessary because we only pay via Upwork. You can access it by logging in, and then clicking on your name. It'll look like this. If you don't already have an account, sign up for one here.
    4. Copy the format below and paste it in a comment on this issue. Replace the placeholder text with your actual details.
      Screen Shot 2022-11-16 at 4 42 54 PM
      Format:
    Contributor details
    Your Expensify account email: <REPLACE EMAIL HERE>
    Upwork Profile Link: <REPLACE LINK HERE>
    
  10. ashusatyarthi-wq commented on Sep 25, 2026

    @ashusatyarthi-wq

    Contributor details
    Your Expensify account email: ashusatyarthi@gmail.com
    Upwork Profile Link: https://www.upwork.com/freelancers/~010e53044b5828fe60

  11. melvin-bot commented on Sep 25, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  12. 17 remaining items

  13. melvin-bot commented on Oct 3, 2026

    @melvin-bot

    @mkhutornyi Uh oh! This issue is overdue by 2 days. Don't forget to update your issues!

  14. mkhutornyi commented on Oct 4, 2026

    @mkhutornyi
    Contributor

    Contributor+ Checklist:

    • [Contributor] The offending PR and associated issue have been commented on, pointing out the bug it caused and why, so the author and reviewers can learn from the mistake.

      Link to the comment on the PR:
      Link to the comment on the Issue:

    • [Contributor] If the regression was CRITICAL (e.g. interrupts a core flow) A discussion in #expensify-open-source has been started about whether any other steps should be taken (e.g. updating the PR review checklist) in order to catch this type of bug sooner.

      Link to discussion:

    • [Contributor] If it was decided to create a regression test for the bug, please propose the regression test steps using the template below to ensure the same bug will not reach production again.

    Regression Test Proposal

    Precondition:

    an account with 50+ expenses (so "Select all" is offered and there is more than one page).

    Test:

    1. Go to Spend > Expenses and wait for the list to finish loading.
    2. With nothing selected, tap the header checkbox → Select all.
    3. Verify the footer shows the expense count and total spend, and never blanks to a loading skeleton after those values appear.

    Do we agree 👍 or 👎

  15. added
    Awaiting PaymentAuto-added when associated PR is deployed to production
    and removed on Oct 4, 2026
  16. melvin-bot commented on Oct 6, 2026

    @melvin-bot

    Triggered auto assignment to @mallenexpensify (Awaiting Payment)

  17. melvin-bot commented on Oct 6, 2026

    @melvin-bot

    Payment Summary

    Resolving PRs:

    BugZero Checklist (@mallenexpensify)

    • I have confirmed assignees, roles, and Upwork contracts look correct
    • I have paid out Upwork contracts / manual NewDot requests
    • [BugZero Assignee] I have created a GH issue for creating/updating the regression test once above steps have been agreed upon
  18. mallenexpensify commented on Oct 8, 2026

    @mallenexpensify
    Contributor

    Payment Summary

    Contributor+: @mkhutornyi due $175 via NewDot

  19. trjExpensify commented on Oct 15, 2026

    @trjExpensify
    Contributor

    approved $175 for @mkhutornyi

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Awaiting PaymentAuto-added when associated PR is deployed to productionBugSomething is broken. Auto assigns a BugZero manager.DailyKSv2ExternalAdded to denote the issue can be worked on by a contributor

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions