Skip to content

[$175] Copilot - Switch button disappears briefly when switching Copilot mode #102444

Open

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: #102124
Email or phone of affected tester (no customers): emady.syed123+cop2@gmail.com
Issue reported by: Applause Internal Team
Bug source: Pull Request QA execution
Device used: Realme C21Y / Android 11
App Component: User Settings

Action Performed:

Precondition : Account A has copilot access to Account B

  1. Sign in as Account A
  2. Go to Settings and switch to Account B
  3. Switch back to Account A

Expected Result:

Switch button should stays visible when switching Copilot mode.

Actual Result:

Switch button disappears briefly when switching Copilot mode

Workaround:

Unknown

Platforms:

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

Screenshots/Videos

Bug7271866_1790622584477.XRecorder_20260928_02.mp4

View all open jobs on GitHub

Applause Internal Information
Exported by: Svetlana Lazutkina
Bug ID: 7271866

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

Activity

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

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

    @MelvinBot
    Contributor

    Proposal

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

    When you switch into or out of a Copilot account, the Switch button disappears until the app finishes reloading.

    What is the root cause of that problem?

    The button only renders when account.delegatedAccess has a delegate or delegators. Switching Copilot mode clears Onyx, and ACCOUNT is not preserved. delegatedAccess is empty until OpenApp returns, so the button returns null during that time.

    • On narrow layouts (Android, iOS), AccountSwitcher already remembers that the user could switch. But it only reserves a blank row, so the button still vanishes.
    • On wide layouts (Windows Chrome), the top-bar AccountSwitcherButton has no such logic, so it simply disappears.
    Code references

    The bug is older than Block migrated user welcome modal in copilot access. That PR's QA only surfaced it.

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

    Move the "was able to switch" latch from AccountSwitcher into AccountSwitcherButton, so both placements share it.

    1. In AccountSwitcherButton, read IS_LOADING_APP and HAS_LOADED_APP. Remember the last canSwitchAccounts value while no switch is in progress.
    2. Render the button when canSwitchAccounts, or when a switch is in progress and the user could switch before.
    3. While the account data reloads, disable the button with stayNormalOnDisable. This stops the user from opening a menu that lists only the current user, without a visual flash.
    4. Remove the latch and blank placeholder from AccountSwitcher.tsx.
    5. Update tests/ui/AccountSwitcherTest.tsx. The test "reserves the Switch button row while an account switch reloads" should now expect the Switch button to stay visible. Add the same check for the wide-layout button.

    What alternative solutions did you explore? (Optional)

    Keep a stub delegatedAccess in ACCOUNT through the Onyx clear. This works for connect, where the delegate is known. It fails for disconnect, because Account A's delegator list is not available then. A stale value could also persist if OpenApp omits it.


    Next Steps for Contributor+ team:
    To accept: @MelvinBot implement [this](https://github.com/Expensify/App/issues/102444) 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 28, 2026
  5. melvin-bot commented on Sep 28, 2026

    @melvin-bot

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

  6. changed the title [-]Copilot - Switch button disappears briefly when switching Copilot mode[/-] [+][$175] Copilot - Switch button disappears briefly when switching Copilot mode[/+] on Sep 28, 2026
  7. melvin-bot commented on Sep 28, 2026

    @melvin-bot
  8. franklincg commented on Sep 28, 2026

    @franklincg

    Proposal

    Root cause

    The transient disappearance is caused by the delegate transition intentionally clearing ONYXKEYS.ACCOUNT before OpenApp repopulates it.

    AccountSwitcherButton derives visibility directly from:

    canSwitchAccountsSelector(account)

    and returns null as soon as that becomes false. During connect() / disconnect(), clearOnyxForDelegateTransition() clears the account payload while keeping IS_LOADING_APP/HAS_LOADED_APP, so delegatedAccess temporarily disappears even though this is only a switch-in-progress state.

    The narrow-layout AccountSwitcher already detects this transition and remembers that switching was available, but it only renders a placeholder row. The wide-layout button has no equivalent transition handling and disappears outright.

    Fix

    I would not preserve the whole ACCOUNT object through the delegate switch, because that risks exposing stale delegate/account data while auth/session identity is changing.

    Instead, keep the control visually stable during the known in-flight window:

    1. In AccountSwitcherButton, read IS_LOADING_APP and HAS_LOADED_APP.
    2. Track the last stable canSwitchAccounts value, updating it only when a delegate switch is not in flight.
    3. Render the Switch button when either:
      • the current account can switch, or
      • switching was available immediately before the current in-flight delegate transition.
    4. While the account payload is temporarily unavailable, keep the button visible but disabled and do not open the menu until fresh delegatedAccess has returned.
    5. Reuse the same transition-visible state in AccountSwitcher so narrow and wide layouts behave consistently, replacing the current blank placeholder behavior.

    This keeps the UI stable without retaining stale account/delegation data across an auth transition.

    Validation

    • narrow layout: Switch stays visible while switching A → B and B → A;
    • wide layout: same behavior;
    • transition button is non-interactive until fresh delegate data returns;
    • account with no delegation never gains a Switch button;
    • failed switch restores the last stable visibility correctly;
    • normal non-switch app loading does not make the button appear.
  9. bonnhatnguyen commented on Sep 28, 2026

    @bonnhatnguyen
    ## Proposal

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

    When a user switches into or out of Copilot mode (delegated access), the "Switch" account button disappears briefly during the reload transition. On narrow layouts, the button vanishes and is temporarily replaced by an empty row placeholder, while on wide layouts (e.g. desktop/web top bar in InitialSettingsPage.tsx), the button disappears completely and abruptly shifts surrounding header layout until the app completes reloading.


    What is the root cause of that problem?

    1. Onyx clearing on delegate transition:
      In src/libs/actions/Delegate.ts, connect() and restoreDelegateSession() call clearOnyxForDelegateTransition(), which purges ONYXKEYS.ACCOUNT to prevent displaying stale account or session data across user boundaries.
      While ONYXKEYS.HAS_LOADED_APP is preserved in KEYS_TO_PRESERVE_DELEGATE_ACCESS and ONYXKEYS.IS_LOADING_APP is seeded to true, account.delegatedAccess is temporarily reset to default/undefined until openApp() returns fresh data from the server.

    2. Early exit in AccountSwitcherButton.tsx:
      In src/components/AccountSwitcherButton.tsx (lines 53 & 276–278):

      const canSwitchAccounts = canSwitchAccountsSelector(account);
      ...
      if (!canSwitchAccounts) {
          return null;
      }

      canSwitchAccountsSelector(account) evaluates (account?.delegatedAccess?.delegators?.length ?? 0) > 0 || !!account?.delegatedAccess?.delegate.
      As soon as account is wiped during the delegate switch, canSwitchAccounts immediately evaluates to false, causing AccountSwitcherButton to return null and unmount.

    3. Incomplete narrow-only latch in AccountSwitcher.tsx:
      AccountSwitcher.tsx attempted a partial workaround using wasAbleToSwitchAccounts and shouldReserveSwitchButtonRow:

      const shouldReserveSwitchButtonRow = shouldShowSwitchButton && shouldStackHeader && wasAbleToSwitchAccounts && !canSwitchAccounts && isAccountSwitchInFlight;

      This approach has two fatal shortcomings:

      • It renders an invisible blank View placeholder (testID={CONST.ACCOUNT_SWITCHER_BUTTON_PLACEHOLDER_TEST_ID}) instead of keeping the actual button visible. The button still visually vanishes.
      • It only executes on narrow stacked headers (shouldStackHeader === true). On wide layouts, InitialSettingsPage.tsx renders <AccountSwitcherButton /> directly in the top bar with no parent wrapper, causing the button to vanish without any latch.

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

    Move the transition latching logic directly into AccountSwitcherButton.tsx so both wide and narrow layouts consistently keep the button visible, while disabling interaction during the brief in-flight data fetch:

    1. In src/components/AccountSwitcherButton.tsx:

      • Subscribe to ONYXKEYS.IS_LOADING_APP and ONYXKEYS.HAS_LOADED_APP:
        const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP);
        const [hasLoadedApp] = useOnyx(ONYXKEYS.HAS_LOADED_APP);
        const isAccountSwitchInFlight = !!isLoadingApp && !!hasLoadedApp;
      • Track wasAbleToSwitchAccounts using state:
        const [wasAbleToSwitchAccounts, setWasAbleToSwitchAccounts] = useState(canSwitchAccounts);
        if (!isAccountSwitchInFlight && wasAbleToSwitchAccounts !== canSwitchAccounts) {
            setWasAbleToSwitchAccounts(canSwitchAccounts);
        }
      • Determine button visibility:
        const shouldRenderButton = canSwitchAccounts || (wasAbleToSwitchAccounts && isAccountSwitchInFlight);
        if (!shouldRenderButton) {
            return null;
        }
      • When isAccountSwitchInFlight && !canSwitchAccounts, render the button with isDisabled and stayNormalOnDisable (or suppress onPress), ensuring the button does not trigger the delegator menu while the fresh account payload is resolving, while maintaining seamless visual continuity without flashing or shifting.
    2. In src/components/AccountSwitcher.tsx:

      • Remove wasAbleToSwitchAccounts, shouldReserveSwitchButtonRow, and the placeholder View (CONST.ACCOUNT_SWITCHER_BUTTON_PLACEHOLDER_TEST_ID).
      • AccountSwitcherButton now handles its own state stability natively across all screens and layouts.
    3. Automated Unit / UI Tests:

      • In tests/ui/AccountSwitcherTest.tsx:
        Update the test 'reserves the Switch button row while an account switch reloads the account data':
        Verify that screen.getByText(SWITCH_BUTTON_TEXT) remains on screen throughout clearOnyxForDelegateTransition() and reload.
        Verify the button is disabled while isAccountSwitchInFlight is true.
        Verify that when a delegator genuinely revokes access outside of an in-flight transition (isAccountSwitchInFlight: false), the button disappears cleanly as expected.

    What alternative solutions did you explore? (Optional)

    • Preserving ONYXKEYS.ACCOUNT in KEYS_TO_PRESERVE_DELEGATE_ACCESS:
      Rejected because preserving ACCOUNT during a delegate transition leaks stale permissions, user flags, and session metadata from Account A into Account B before openApp returns.
    • Rendering a placeholder icon in InitialSettingsPage.tsx:
      Rejected as it introduces duplicated layout workaround logic across pages. Encapsulating the transition latch within AccountSwitcherButton provides a single source of truth for both wide and narrow layouts.

    Contributor details
    Your Expensify account email: bonnhatnguyen0@gmail.com
    Upwork Profile Link: https://www.upwork.com/freelancers/~0139ed4098bdc50182

  10. melvin-bot commented on Sep 28, 2026

    @melvin-bot

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

  11. bconnnnn commented on Sep 29, 2026

    @bconnnnn

    I think the disappearing button is just a transient state during the delegate switch, not a permissions change.

    AccountSwitcherButton decides whether it exists from canSwitchAccountsSelector(account), and returns null as soon as that becomes false. During the Copilot transition, ACCOUNT is cleared before OpenApp repopulates it, so delegatedAccess disappears for a moment and the button gets unmounted.

    I wouldn't preserve the whole old ACCOUNT object through the switch because that can leave stale account data visible while identity is changing.

    A smaller fix is to keep the switcher rendered during the known account-switch loading window if it was available immediately before the transition, then let the fresh ACCOUNT value take over once loading finishes.

    That also gives us consistent behavior between narrow and wide layouts instead of the current blank-placeholder vs disappearing-button split.

    I'd add a test that starts with a switchable account, clears ACCOUNT while the app is in the delegate transition, and verifies the Switch control stays mounted until the new account data arrives.

  12. bconnnnn commented on Sep 29, 2026

    @bconnnnn

    Proposal

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

    When switching into or out of Copilot mode, the Switch control disappears during the app reload and returns after the new account data is loaded. This is visible as a blank row in the narrow stacked settings header and as a disappearing control/layout shift in the wide settings top bar.

    What is the root cause of that problem?

    canSwitchAccountsSelector reads account.delegatedAccess. The delegate transition clears Onyx while preserving HAS_LOADED_APP and setting IS_LOADING_APP=true; ACCOUNT is not preserved, so the selector temporarily returns false until openApp() supplies the next account payload. AccountSwitcherButton immediately returns null when the selector is false. AccountSwitcher remembers prior eligibility only to reserve a blank row in the narrow layout, while InitialSettingsPage renders the button directly in the wide top bar.

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

    Move transition visibility handling into AccountSwitcherButton so both layouts render the same control. While IS_LOADING_APP && HAS_LOADED_APP marks a delegate transition, retain only the last stable boolean eligibility, not the previous ACCOUNT object. If switching was available immediately before the transition, keep the button mounted but disabled and visually normal until fresh account data arrives; then use the new account's eligibility and hide the button if the user can no longer switch. This keeps the menu from opening with stale delegation data.

    Remove the narrow-only eligibility latch and blank placeholder from AccountSwitcher, since the button itself would own visibility during the transition.

    What tests should be added?

    Update AccountSwitcherTest.tsx to exercise the real clearOnyxForDelegateTransition() path. Assert that the Switch button stays visible but cannot open its menu while the account is loading, then becomes enabled after fresh switchable account data arrives. Also cover a transition where fresh data removes switching access, the direct wide-top-bar placement, and a user who never had switching access.

    What alternative solutions did you explore? (Optional)

    Preserving the full previous ACCOUNT object would expose stale account/delegation data during an identity transition. A blank placeholder reserves space but does not preserve the control and does not address the wide-layout case.

    Direct implementation sketch

    Track whether the control was available before the app reload, and keep it rendered only during the in-flight transition. Do not retain the old ACCOUNT object.

    const isAccountSwitchInFlight = Boolean(isLoadingApp && hasLoadedApp);
    const wasSwitchableBeforeLoad = useRef(canSwitchAccounts);
    
    useEffect(() => {
        if (!isAccountSwitchInFlight) {
            wasSwitchableBeforeLoad.current = canSwitchAccounts;
        }
    }, [canSwitchAccounts, isAccountSwitchInFlight]);
    
    const shouldRenderSwitchControl =
        canSwitchAccounts || (isAccountSwitchInFlight && wasSwitchableBeforeLoad.current);

    Use this visibility condition in AccountSwitcherButton so it covers the wide top-bar path too; keep activation disabled while the account payload is being replaced. Test a switchable-to-switchable transition, a switchable-to-non-switchable transition, and a user who has never had switch access.

  13. bconnnnn commented on Sep 30, 2026

    @bconnnnn

    @MelvinBot @gijoe0295 I added a concrete transition-state approach and tests for both narrow and wide layouts, without preserving the old ACCOUNT payload. I’m available to implement it; could you review the updated proposal?

  14. melvin-bot commented on Oct 1, 2026

    @melvin-bot

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

  15. melvin-bot commented on Oct 5, 2026

    @melvin-bot

    @gijoe0295 Still overdue 6 days?! Let's take care of this!

  16. melvin-bot commented on Oct 7, 2026

    @melvin-bot

    @gijoe0295 8 days overdue is a lot. Should this be a Weekly issue? If so, feel free to change it!

  17. melvin-bot commented on Oct 9, 2026

    @melvin-bot

    @gijoe0295 10 days overdue. Is anyone even seeing these? Hello?

  18. gijoe0295 commented on Oct 12, 2026

    @gijoe0295
    Contributor

    This is a pretty nit bug. Not sure if we want to fix this. If so, Melvin's proposal LGTM. Will leave final decision to the internal engineer.

    🎀 👀 🎀 C+ reviewed

  19. melvin-bot commented on Oct 12, 2026

    @melvin-bot

    Triggered auto assignment to @puneetlath, see https://stackoverflow.com/c/expensify/questions/7972 for more details.

  20. puneetlath commented on Oct 12, 2026

    @puneetlath
    Contributor

    It makes sense to me. Let's go ahead and have Melvin implement it.

  21. melvin-bot commented on Oct 12, 2026

    @melvin-bot

    @puneetlath @gijoe0295 this issue was created 2 weeks ago. Are we close to approving a proposal? If not, what's blocking us from getting this issue assigned? Don't hesitate to create a thread in #expensify-open-source to align faster in real time. Thanks!

  22. gijoe0295 commented on Oct 13, 2026

    @gijoe0295
    Contributor

    @MelvinBot implement this

  23. MelvinBot commented on Oct 13, 2026

    @MelvinBot
    Contributor

    🤖 Draft PR created: Keep the Copilot Switch button visible while switching accounts
    Please mark it as "Ready for review" or ask me to do that for you when it is ready for review.

    The PR implements the approved proposal:

    • The Switch button stays visible in both the narrow and wide layouts while a Copilot switch reloads the account data.
    • The button can't be pressed until the account data loads, but it doesn't look disabled.
    • It still hides if the account you land on can't switch.

    All 12 tests in tests/ui/AccountSwitcherTest.tsx pass. The two new tests fail on main. I couldn't check the change in a web browser because the test session stuck on a blank page, so it still needs a manual check.

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

Metadata

Metadata

Labels

BugSomething 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