Skip to content

[Exploratory] FAB button is lost when navigate back from Concierge RHP to Inbox #100521

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.73-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: https://test-management.browserstack.com/projects/2219752/test-runs/72690978/folder/13176682/75343319/1733026366?sort=%2Bstatus_id
Email or phone of affected tester (no customers): gocemate+buzz1961@gmail.com
Issue reported by: Applause Internal Team
Bug source: Exploratory - Significant User Experience Deterioration
Device used: iphone 11/26.2
App Component: Other

Action Performed:

  1. Open iOS app
  2. Go to Spend tab> Tap on Concierge chat button in the upper-right corner
  3. When Concierge page open tap Back button
  4. Go to Inbox tab> Tap on Concierge chat button in the upper-right corner
  5. When Concierge page open tap Back button

Expected Result:

FAB button should be present

Actual Result:

FAB button is lost

Workaround:

Unknown

Platforms:

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

Screenshots/Videos

Bug7253447_1788788546507.ScreenRecording_09-07-2026_15-32-35_1.mp4

View all open jobs on GitHub

Applause Internal Information
Exported by: Mita Rachim
Bug ID: 7253447

Issue OwnerCurrent Issue Owner: @eh2077

Activity

  1. applause-bot commented on Sep 7, 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/1f695e6016a7a730a1f967af6ffbdd277ee29bba359b3458b07e45ece5821f19

  2. added
    BugSomething is broken. Auto assigns a BugZero manager.
    on Sep 24, 2026
  3. MelvinBot commented on Sep 24, 2026

    @MelvinBot
    Contributor

    Proposal

    What is the root cause of that problem?

    On narrow layout the FAB is rendered only by the navigator-level tab bar. Every tab root screen also renders a second, duplicate tab bar through ScreenWrapper's bottomContent, and that copy hardcodes shouldShowFloatingButtons={false}. So "tab bar is there but the FAB is gone" means the navigator-level bar is stuck hidden and you are looking at the duplicate.

    It gets stuck because isTabRouteAtRoot falls back to the tab route's params whenever the route's nested state is undefined:

    • Opening Concierge sets the Inbox tab route's params to the push target Report.
    • A back navigation pops the nested stack but never clears those tab-level params — the file's own comment says they "can be stale after within-tab back navigation".
    • The tab navigator runs with lazy: true and freezeOnBlur: true on native. React Navigation blanks a frozen screen's cached state back to undefined, which is what the first half of the repro (Spend → Concierge → Back → switch tabs) sets up.

    With state undefined and params still saying Report — a screen that is not in the tab-root allow-list — isAtRoot is false, so shouldHide stays true and the bar is rendered at opacity 0 with shouldShowFloatingButtons={false}.

    This is the remaining hole in [CP Staging] fix: always show FAB on the root-level, which made the focused leaf authoritative but kept the stale-params fallback for the state === undefined case.

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

    Stop trusting params once a route has hydrated. The push-target fallback exists only so the bar doesn't flash on Android cold start, so scope it to that: track per route.key whether a real nested state has ever been observed, and after the first observation resolve isAtRoot from the last known focused leaf instead of falling back to getPushTargetLeaf(route.params). A frozen or not-yet-rendered tab then keeps its last correct answer rather than resurrecting a screen the user already backed out of.

    What alternative solutions did you explore? (Optional)

    • Clear the tab route's params when a within-tab back navigation pops the nested stack. Fixes the source of the staleness but touches the custom routers, so it is riskier.
    • Turn off freezeOnBlur for the tab navigator. Removes the trigger but gives up the performance win.
    Investigation details and what I could not verify

    Ruled out — the animationDoneKey / shouldApplyDelay gate. usePrevious(shouldHide) clears shouldApplyDelay on the very next render, and the requestAnimationFrame effect re-runs on every stateKey change, so the delay is transient by construction. isHidden can only latch if shouldHide latches.

    Ruled out — isBlockingViewVisible. Its only writer is ForceFullScreenView, and the native variant is a pure passthrough that never registers a route key, so the value is permanently false on iOS.

    Repro asymmetry explained. Opening Concierge from Spend crosses navigators and pushes a whole new TAB_NAVIGATOR, so backing out changes state.index and the bar recovers. Opening it from Inbox stays in the same tab route, which is why only the second pass shows the bug.

    Not verified at runtime. I could not confirm on a device that activeRoute.state is actually undefined at that moment. The mechanism depends on native freeze behavior, so a web run cannot validate it (my web session also crashed to a blank page before reaching the flow). The quickest confirmation: enable debug mode on the device — DebugTabView already displays isTabRouteAtRoot — or log {index, params, state} from TabNavigatorBar.

    Confidence: Medium. The code path is fully traced, but it hinges on the unverified runtime assumption above.

    Related issues: a quick scan of open Expensify/App issues found no duplicate or closely related report.


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


    view run · no recording available

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

    @melvin-bot

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

  6. melvin-bot commented on Sep 24, 2026

    @melvin-bot

    Unable to auto-create job on Upwork. The BZ team member should create it manually for this issue.

  7. ashusatyarthi-wq commented on Sep 26, 2026

    @ashusatyarthi-wq
    Contributor details Your Expensify account email: ashu.satyarthi.business@gmail.com Upwork Profile Link: https://www.upwork.com/freelancers/~01b8ab177d61dc225c

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    On narrow/mobile screens (iOS/Android), after opening Concierge from the Spend tab, backing out, switching to the Inbox tab, opening Concierge again, and backing out to Inbox, the floating action button (FAB) disappears from the screen.

    What is the root cause of that problem?

    In narrow layout, the FAB is exclusively rendered by the navigator-level tab bar (TabNavigatorBar.tsx). Individual tab root screens also render a bottom tab bar via ScreenWrapper.bottomContent, but that duplicate copy hardcodes shouldShowFloatingButtons={false}. When the FAB disappears while the tab bar is visible, it means the navigator-level tab bar is hidden (shouldHide = true), leaving only the duplicate bottom content visible.

    The navigator-level bar hides when isTabRouteAtRoot(activeRoute) returns false.
    In src/libs/Navigation/helpers/isTabRouteAtRoot.ts:55-58:

    function isTabRouteAtRoot(route: TabRouteLike | undefined): boolean {
        const focusedLeaf = getFocusedLeafScreenName(route?.state);
        return isAtTabRootLevel(focusedLeaf ?? getPushTargetLeaf(route?.params));
    }
    1. When opening Concierge from a tab, route.params receives the push target params (e.g. {screen: 'Report', params: ...}).
    2. When the user navigates back, the nested navigation stack pops back to root, but React Navigation does not clear route.params (as noted in the file: "Tab-level params can be stale after within-tab back navigation").
    3. On native platforms, the tab navigator is configured with freezeOnBlur: true. When a tab is unfocused and refocused, React Navigation freezes the screen and can reset route.state to undefined before rehydration.
    4. When route.state is undefined, focusedLeaf evaluates to undefined.
    5. isTabRouteAtRoot falls back to getPushTargetLeaf(route?.params). Since route.params still carries the stale push target (Report), and Report is not in SCREENS_WITH_TAB_BAR, isAtTabRootLevel evaluates to false.
    6. As a result, isAtRoot becomes false, shouldHide becomes true, and TabNavigatorBar hides the navigator tab bar, removing the FAB.

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

    In src/libs/Navigation/helpers/isTabRouteAtRoot.ts:
    Track the last known focused leaf by route.key so that once a tab route has hydrated and resolved its root state, a temporary route.state === undefined (e.g. from freezeOnBlur) retains the last resolved state rather than resurrecting stale push params:

    type TabRouteLike = {
        key?: string;
        state?: NavigationState | PartialState<NavigationState>;
        params?: unknown;
    };
    
    const lastKnownFocusedLeafByRouteKey = new Map<string, string>();
    
    function isTabRouteAtRoot(route: TabRouteLike | undefined): boolean {
        const focusedLeaf = getFocusedLeafScreenName(route?.state);
        if (route?.key && focusedLeaf) {
            lastKnownFocusedLeafByRouteKey.set(route.key, focusedLeaf);
        }
        const resolvedLeaf = focusedLeaf ?? (route?.key ? lastKnownFocusedLeafByRouteKey.get(route.key) : undefined) ?? getPushTargetLeaf(route?.params);
        return isAtTabRootLevel(resolvedLeaf);
    }

    The fallback to getPushTargetLeaf(route?.params) is reserved strictly for initial cold starts before the route's navigator state has ever rendered. Once the user has navigated and popped back, lastKnownFocusedLeafByRouteKey preserves the current root view leaf.

    What alternative solutions did you explore? (Optional)

    1. Disabling freezeOnBlur: true in TabNavigator.native.tsx was considered, but that forfeits native memory optimization and background view suspension.
    2. Manually stripping route.params on back navigation inside custom routers was explored, but mutating route params during stack unwinds carries broader side effects for deep linking.
      Tracking the last resolved leaf for the route key is lightweight, self-contained within isTabRouteAtRoot.ts, and prevents stale params from falsely hiding the FAB.
  8. melvin-bot commented on Sep 29, 2026

    @melvin-bot

    @eh2077 Huh... This is 4 days overdue. Who can take care of this?

  9. 4 remaining items

  10. melvin-bot commented on Oct 6, 2026

    @melvin-bot

    @eh2077 Huh... This is 4 days overdue. Who can take care of this?

  11. eh2077 commented on Oct 7, 2026

    @eh2077
    Contributor

    I can't reproduce with iOS simulator. @MelvinBot check this issue again please

    Screen.Recording.2026-10-07.at.9.58.19.PM.mov
  12. MelvinBot commented on Oct 7, 2026

    @MelvinBot
    Contributor

    Thanks eh2077. I rechecked this against current main (9.5.4-0). I think the bug is most likely already gone, and I suggest you don't implement my earlier proposal.

    • No PR fixed it directly. The FAB visibility code, the Concierge button's navigation, and the Back path are basically the same as in 9.4.73-0, where the bug was reported. The route.params fallback is still in isTabRouteAtRoot.ts:55-58.
    • One later PR probably fixed it as a side effect: perf: preload the Reports tab so the first Inbox open runs the warm path, merged 2026-09-21 and first shipped in 9.4.90-2. After the app finishes loading, it mounts the Inbox tab in the background (AuthScreensInitHandler.tsx:256-276). My earlier analysis depended on the Inbox tab having no navigation state yet, and the preload removes that condition.
    • My earlier root cause is still unconfirmed. When I traced the code again, I found no path that leaves the Inbox tab with no navigation state, in either version. Treat the proposal as unverified.

    Suggested next step: ask Applause to retest on a current build on a physical iPhone. If they can't reproduce it, close the issue. The reporter used a real iPhone 11 on iOS 26.2, so your simulator result alone doesn't fully rule the bug out.

    Other changes since 9.4.73-0 that I ruled out

    I didn't run a web test, because this bug depends on native iOS screen freezing and web can't reproduce it.


    view run

  13. melvin-bot commented on Oct 12, 2026

    @melvin-bot

    @eh2077 Eep! 4 days overdue now. Issues have feelings too...

  14. eh2077 commented on Oct 14, 2026

    @eh2077
    Contributor

    @MelvinBot since it isn't reproducible anymore, can you ask QA team for retest, then we can close it more confidently!

  15. MelvinBot commented on Oct 14, 2026

    @MelvinBot
    Contributor

    @Expensify/applauseleads please retest this on the latest build, on a physical iPhone if possible.

    eh2077 can't reproduce it on an iOS simulator, and the bug may have been fixed as a side effect of a later change. Please use the steps in the issue description and confirm whether the FAB still disappears after step 5.

    If you can't reproduce it, we'll close the issue.


    view run

  16. melvin-bot commented on Oct 19, 2026

    @melvin-bot

    @eh2077 Eep! 4 days overdue now. Issues have feelings too...

  17. eh2077 commented on Oct 20, 2026

    @eh2077
    Contributor

    Not reproducible #100521 (comment), closing

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

Metadata

Metadata

Assignees

Labels

BugSomething is broken. Auto assigns a BugZero manager.DailyKSv2ExternalAdded to denote the issue can be worked on by a contributorNot a priority

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions