Skip to content

[$175] Payments - Previously connected bank account remains visible after disabling and re-enabling Payments until refresh #105918

Open

Description

@MelvinBot

Version Number: Not provided.
Reproducible in staging?: Yes, confirmed by dukenv0307 in the source discussion.
Reproducible in production?: Yes, confirmed in the same discussion.
If this was caught during regression testing, add the test name, ID and link from BrowserStack: Not provided.
Email or phone of affected tester (no customers): Not provided.
Logs: Not provided.
Expensify/Expensify Issue URL: Not provided.
Issue reported by: bernhardoj; also confirmed by dukenv0307.
Slack conversation: Not provided; reported on GitHub.

Action Performed:

Precondition: You are a workspace admin, Payments is enabled, and a business bank account is connected to the workspace.

  1. Open the workspace in New Expensify and go to Workflows > Payments.
  2. Turn Payments off. Confirm the warning if one appears.
  3. Turn Payments back on without refreshing the page or reconnecting the bank account.
  4. Observe the bank account shown in Payments.
  5. Refresh the page and observe the bank account again.

Expected Result:

Disabling Payments removes the workspace's bank-account connection from the UI without requiring a refresh. Re-enabling Payments should not display the previous account as connected until you relink it.

The bank account should remain available in your Expensify account; this concerns its connection to the workspace, not deletion of the account.

Actual Result:

After Payments is turned off and back on, the previous bank account still appears connected. It disappears after a refresh.

In the reviewer's report, policy.achAccount is undefined, but bankAccountList still links the bank account to the workspace. This is a reported observation, not a newly verified root cause.

Workaround:

Refresh the page to clear the stale connected-account display. dukenv0307 confirmed this workaround and reproduction on staging/production. No independent reproduction was run when filing this issue.

Platforms:

The report concerns New Expensify. The exact browser and operating system were not provided.

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

Screenshots/Videos

Reviewer's recording:
https://github.com/user-attachments/assets/c3ff924f-cca4-4f4c-ad51-5dfb941b8bad

Earlier confirmation and recording:
#105333 (comment)

Related context

Filed separately at the request of dukenv0307, outside the scope of Warn admins before turning off Payments when bank account countries or a business bank account are set up.

Potentially related: Clicking the bank account opens “Log into your bank” after toggling Payments off and on. That issue tracks the destination after clicking the stale account; this report tracks the stale connection display and refresh workaround.

Upwork Automation - Do Not Edit

Activity

  1. added
    BugSomething is broken. Auto assigns a BugZero manager.
    on Nov 4, 2026
  2. Hazem20wosis commented on Nov 4, 2026

    @Hazem20wosis

    Proposal

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

    Payments - When disabling payments on a workspace and then re-enabling them, the previously connected bank account still displays as connected in the UI until the application is refreshed.

    What is the root cause of that problem?

    In WorkflowsPaymentsTab.tsx:

    const bankAccountConnectedToWorkspace = Object.values(bankAccountList ?? {}).find((bankAccount) => bankAccount?.accountData?.additionalData?.policyID === policy?.id);
    const shouldShowBankAccount = (!!isBankAccountFullySetup || !!bankAccountConnectedToWorkspace) && getReimbursementChoice(policy) !== CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO;

    When payments are turned off via the toggle, setWorkspaceReimbursement in src/libs/actions/Policy/Policy.ts sets reimbursementChoice: CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO, clears the policy's achAccount, and sends bankAccountID: 0 to the backend.

    However:

    1. setWorkspaceReimbursement clears policy.achAccount, but it does not clear additionalData.policyID on the bank account in Onyx's bankAccountList.
    2. When payments are disabled, getReimbursementChoice(policy) === REIMBURSEMENT_NO, so shouldShowBankAccount evaluates to false (hidden).
    3. When payments are re-enabled, policy.achAccount is cleared/empty. However, bankAccountConnectedToWorkspace looks solely at bankAccount?.accountData?.additionalData?.policyID === policy?.id in Onyx's bankAccountList. Because the stale policyID reference remains cached in Onyx, bankAccountConnectedToWorkspace evaluates to truthy!
    4. Consequently, shouldShowBankAccount immediately evaluates to true, and the previously connected bank account unexpectedly reappears on screen until a full refresh forces the client to fetch fresh backend data.

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

    Approach 1 (Comprehensive & Clean Component Safeguard - Recommended):

    In src/pages/workspace/workflows/tabs/WorkflowsPaymentsTab.tsx:
    A bank account should only be considered connected to the workspace if the policy actually has a configured or pending setup referencing it, rather than blindly trusting stale additionalData.policyID when policy.achAccount is empty/cleared:

    const isBankAccountConnected = !!policy?.achAccount?.bankAccountID || !!policy?.achAccount?.state;
    const bankAccountConnectedToWorkspace = isBankAccountConnected 
        ? Object.values(bankAccountList ?? {}).find((bankAccount) => bankAccount?.accountData?.additionalData?.policyID === policy?.id)
        : undefined;

    Or alternatively, cross-verify that the connected bank account matches the policy's achAccount:

    const bankAccountConnectedToWorkspace = Object.values(bankAccountList ?? {}).find((bankAccount) => 
        bankAccount?.accountData?.additionalData?.policyID === policy?.id && 
        (!policy?.achAccount || policy?.achAccount?.bankAccountID === bankAccount?.methodID)
    );

    This cleanly eliminates the phantom display across all re-enable flows without risking complex nested array mutations or rollback corruptions on global Onyx keys.

    Approach 2 (Action-side Onyx Sync in Policy.ts):

    In src/libs/actions/Policy/Policy.ts, inside setWorkspaceReimbursement:
    When disabling payments (reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO):
    If oldBankAccountID exists in bankAccountList, include an optimistic update that removes policyID from accountData.policyIDs and sets accountData.additionalData.policyID to null:

    if (reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO && oldBankAccountID) {
        optimisticData.push({
            onyxMethod: Onyx.METHOD.MERGE,
            key: ONYXKEYS.BANK_ACCOUNT_LIST,
            value: {
                [oldBankAccountID]: {
                    accountData: {
                        policyIDs: bankAccountList?.[oldBankAccountID]?.accountData?.policyIDs?.filter((id) => id !== policyID) ?? [],
                        additionalData: {policyID: null},
                    },
                },
            },
        });
    }

    And add corresponding rollbacks in failureData.

    Screenshots/Videos

    Reproducible Video Evidence & Demonstration
    e.mp4

    What alternative solutions did you consider?

    Clearing bankAccountList cache completely, but that is overly aggressive as other workspaces or personal payment methods rely on bankAccountList. Approach 1 is the safest and most surgical solution with zero risk of regressions.

    Contributor details
    Your Expensify account email: hazemhazemahmedsalem@gmail.com
    Upwork Profile Link: https://www.upwork.com/freelancers/~017a671f901fafe3a1

  3. melvin-bot commented on Nov 4, 2026

    @melvin-bot

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

  4. melvin-bot commented on Nov 4, 2026

    @melvin-bot

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

  5. trjExpensify commented on Nov 4, 2026

    @trjExpensify
    Contributor
  6. added
    ExternalAdded to denote the issue can be worked on by a contributor
    on Nov 4, 2026
  7. melvin-bot commented on Nov 4, 2026

    @melvin-bot

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

  8. changed the title [-]Payments - Previously connected bank account remains visible after disabling and re-enabling Payments until refresh[/-] [+][$175] Payments - Previously connected bank account remains visible after disabling and re-enabling Payments until refresh[/+] on Nov 4, 2026
  9. melvin-bot commented on Nov 4, 2026

    @melvin-bot
  10. mountiny commented on Nov 4, 2026

    @mountiny
    Contributor

    Added as critical

  11. Enes2553 commented on Nov 5, 2026

    @Enes2553

    Proposal

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

    When a workspace has a connected business bank account and an admin disables Payments and then re-enables Payments on Workspace > Workflows, the previously connected bank account immediately reappears as connected until the app is refreshed.

    What is the root cause of that problem?

    In src/pages/workspace/workflows/tabs/WorkflowsPaymentsTab.tsx:144-160, the connected bank account UI is driven by two Onyx sources:

    const isBankAccountFullySetup = policy?.achAccount && (policy?.achAccount.state === CONST.BANK_ACCOUNT.STATE.OPEN || policy?.achAccount.state === CONST.BANK_ACCOUNT.STATE.LOCKED);
    const bankAccountConnectedToWorkspace = Object.values(bankAccountList ?? {}).find((bankAccount) => bankAccount?.accountData?.additionalData?.policyID === policy?.id);
    ...
    const shouldShowBankAccount = (!!isBankAccountFullySetup || !!bankAccountConnectedToWorkspace) && getReimbursementChoice(policy) !== CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO;

    When toggling Payments off (reimbursementChoice = CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO), setWorkspaceReimbursement in src/libs/actions/Policy/Policy.ts:1480-1654 sends bankAccountID: 0 to the backend (line 1650), but fails to clear the connected bank account in Onyx:

    1. policy.achAccount is overwritten with the old bank account instead of null (Policy.ts:1517):
      optimisticData merges achAccount: {reimburser: reimburserEmail, bankAccountID, accountNumber, addressName, bankName, state} into ${ONYXKEYS.COLLECTION.POLICY}${policyID} even when shouldClearBankAccountID (reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO) is true.
    2. ONYXKEYS.BANK_ACCOUNT_LIST retains the workspace's policyID (WorkflowsPaymentsTab.tsx:324 & Policy.ts:1498-1549):
      • WorkflowsPaymentsTab.tsx:324 does not pass bankAccountList to setWorkspaceReimbursement.
      • oldBankAccountID in Policy.ts:1498-1501 only checks account?.accountData?.policyIDs?.includes(policyID) and misses account?.accountData?.additionalData?.policyID === policyID.
      • The early-return guard at line 1505 (if (oldBankAccountID !== undefined && String(bankAccountID) === oldBankAccountID && currentAchAccount?.bankAccountID === bankAccountID)) is not guarded by !shouldClearBankAccountID.
      • Lines 1536–1542 unconditionally write policyID back into optimisticBankAccountList[bankAccountID] even when disabling payments.
    3. Re-enabling Payments re-sends the stale bank account (WorkflowsPaymentsTab.tsx:317-337):
      Because isBankAccountFullySetup and bankAccountConnectedToWorkspace remain truthy in Onyx after disabling Payments, toggling Payments back on evaluates (!isBankAccountFullySetup && !bankAccountConnectedToWorkspace) as false, selects REIMBURSEMENT_YES instead of REIMBURSEMENT_MANUAL, and passes the stale policy?.achAccount?.bankAccountID back to setWorkspaceReimbursement.

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

    1. In src/pages/workspace/workflows/tabs/WorkflowsPaymentsTab.tsx:324-337:
      Pass bankAccountList to setWorkspaceReimbursement (and only pass policy?.achAccount details when enabling with an active account):

      setWorkspaceReimbursement({
          policyID,
          currentAchAccount: policy?.achAccount,
          currentReimbursementChoice: policy?.reimbursementChoice,
          currentIsCollectDepositAccountsEnabled: policy?.isCollectDepositAccountsEnabled,
          currentReimbursementCountries: policy?.reimbursement?.countries,
          reimbursementChoice: newReimbursementChoice,
          reimburserEmail: newReimburserEmail ?? '',
          bankAccountID: policy?.achAccount?.bankAccountID,
          accountNumber: policy?.achAccount?.accountNumber,
          addressName: policy?.achAccount?.addressName,
          bankName: policy?.achAccount?.bankName,
          state: policy?.achAccount?.state,
          bankAccountList,
      });
    2. In src/libs/actions/Policy/Policy.ts:1496-1651 (setWorkspaceReimbursement):

      • Compute const shouldClearBankAccountID = reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO; at the top of setWorkspaceReimbursement.
      • Match oldBankAccountID against both policyIDs and additionalData.policyID:
        const oldBankAccountID = Object.keys(bankAccountList ?? {}).find((accountID) => {
            const account = bankAccountList?.[accountID];
            return account?.accountData?.policyIDs?.includes(policyID) || account?.accountData?.additionalData?.policyID === policyID;
        });
      • Guard the redundant-selection early return at line 1505 with !shouldClearBankAccountID:
        if (!shouldClearBankAccountID && oldBankAccountID !== undefined && String(bankAccountID) === oldBankAccountID && currentAchAccount?.bankAccountID === bankAccountID) {
            return;
        }
      • Clear achAccount in optimisticData when shouldClearBankAccountID is true:
        achAccount: shouldClearBankAccountID ? null : {reimburser: reimburserEmail, bankAccountID, accountNumber, addressName, bankName, state},
      • In the ONYXKEYS.BANK_ACCOUNT_LIST optimistic and failure updates:
        • When oldBankAccountID exists, remove policyID from accountData.policyIDs and set accountData.additionalData.policyID to null (with corresponding rollback in failureData).
        • Only assign policyID onto optimisticBankAccountList[bankAccountID] (and failureBankAccountList[bankAccountID]) when !shouldClearBankAccountID && bankAccountID !== undefined.

    What alternative solutions did you explore? (Optional)

    • Only gating bankAccountConnectedToWorkspace on !!policy?.achAccount?.bankAccountID in WorkflowsPaymentsTab.tsx: Rejected because partially set-up bank accounts (SETUP, PENDING, VERIFYING checked via isBankAccountPartiallySetup(state) at line 155) do not populate policy.achAccount and rely on bankAccountList[id].accountData.additionalData.policyID === policy.id. Leaving stale policyID entries in ONYXKEYS.BANK_ACCOUNT_LIST also breaks getEligibleExistingBusinessBankAccounts at line 158.
  12. FitseTLT commented on Nov 5, 2026

    @FitseTLT
    Contributor

    @MelvinBot can u put a proposal

  13. MelvinBot commented on Nov 5, 2026

    @MelvinBot
    ContributorAuthor

    Proposal

    What is the root cause of that problem?

    Turning Payments off clears the bank account in the API request, but the optimistic update retains the old policy.achAccount. The Payments toggle also omits bankAccountList when calling the action, leaving its workspace associations untouched.

    Re-enabling Payments trusts either source, so the stale account reappears—even if a server update has already cleared policy.achAccount.

    Code evidence and verification limits

    This confirms a client-state defect through code inspection. I have not independently reproduced the UI behavior or inspected server responses; a response that actively restores stale associations would require additional investigation.

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

    Fix the stored state rather than hiding the account in the component:

    1. Pass the existing bankAccountList into the reimbursement action from the Payments toggle.
    2. When disabling Payments, optimistically clear policy.achAccount and detach the workspace from matching bank-list entries. Remove only this workspace from policyIDs; clear additionalData.policyID only when it matches this workspace. Preserve the bank accounts themselves and other workspace associations. Handle accounts associated only through additionalData, including incomplete setups without an ACH account.
    3. Do not let the same-account early return bypass disabling, and do not reattach the selected account during disable. Keep existing account-selection behavior for enabling.
    4. On failure, restore the complete previous ACH object and the association fields changed by this request, alongside the existing reimbursement-settings rollback.
    Regression coverage
    • Disable/re-enable without refresh: the previous account stays disconnected and remains available for explicit relinking.
    • Repeat offline/with requests paused, and verify rollback after a failed disable.
    • Cover absent ACH data, singular-only and array-only associations, and shared accounts belonging to other workspaces.
    • Preserve existing account-selection behavior and legitimate incomplete bank setups.
    • Extend PolicyTest.ts:5650–6058 and the existing incomplete-account UI fixture.

    No code changes or tests were run for this proposal.

    What alternative solutions did you explore? (Optional)

    Requiring policy.achAccount before displaying a bank-list account would hide legitimate incomplete setups. Clearing the entire bank list would affect unrelated accounts. Passing the list alone is also insufficient because the action currently reattaches the selected account.

    Related report: [Exploratory] Selecting same BA, add BA RHP shown after toggle off/on payments concerns the destination after clicking an account following the same toggle sequence; it is potentially related, not a confirmed duplicate.


    Next Steps for Contributor+ team:
    To accept: @MelvinBot implement this proposal to create a draft PR.
    To refine: @MelvinBot <your feedback>
    To reject: Explain why you are rejecting Melvin's proposal.

  14. FitseTLT commented on Nov 5, 2026

    @FitseTLT
    Contributor

    @MelvinBot implement

  15. MelvinBot commented on Nov 5, 2026

    @MelvinBot
    ContributorAuthor

    🤖 Web test steps verification results: ❌ fail

    Blocked on dev NewDot web: the handed-off session showed a blank page. Both initial and post-wait snapshots returned 0 nodes, so the fix could not be verified; no state was seeded or changed.

    Blank dev NewDot page

    Step Status
    Workspace admin, Payments enabled, and connected business bank accountPreconditions could not be inspected: blank page, 0 snapshot nodes, and Workspaces wait timed out.
    Blank dev NewDot page
    ❌
    Payments off/on disconnects the previous bank and shows Add bank accountNot executed because the app UI was unavailable.
    ❌
    Disconnection persists after refreshNot executed; refresh is also prohibited by the web session policy.
    ❌
    Payments off/on offlineNot executed; offline network control is outside supported web CLI capabilities.
    ❌

    Implementation: Created draft PR Detach workspace bank accounts when disabling Payments, assigned to FitseTLT, with PullerBear review requested. It clears the workspace connection, preserves other workspaces' links, and restores state on failure.

    Automated checks passed: 37,160 Jest tests; final focused run of 263 tests; lint, TypeScript, formatting, spelling, component compiler check, Storybook smoke test, and coding-standards review. Command substitutions and the browser blocker are documented in the PR.

    Please complete manual verification before marking the draft ready for review, or ask me to mark it ready after verification.


    view run · no recording available

  16. jeffrey-graham commented on Nov 7, 2026

    @jeffrey-graham
  17. melvin-bot commented on Nov 7, 2026

    @melvin-bot

    📣 @jeffrey-graham! 📣
    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>
    
  18. melvin-bot commented on Nov 7, 2026

    @melvin-bot

    📣 @ronanchnlps-bit! 📣
    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>
    
  19. TassioSales commented on Nov 8, 2026

    @TassioSales

    Proposal

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

    After disabling and re-enabling Payments in a workspace, previously connected bank accounts remain visible in the UI until the page is refreshed.

    What is the root cause of that problem?

    In WorkspacePaymentsPage.tsx and Policy.ts:
    When payments are toggled off, the policy configuration is disabled on the backend, but the local Onyx cache for bankAccountList and workspace reimbursement accounts is not cleared or invalidated locally. When payments are toggled back on, the component reads the lingering cached bank account records from Onyx before the server syncs.

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

    1. In Policy.ts (setWorkspaceReimbursementAccount / disablePayments):
      • When disabling payments for a policy, purge cached policy payment connection records from Onyx optimistically.
    2. In WorkspacePaymentsPage.tsx:
      • Ensure component state re-fetches or subscribes to fresh payment connection status when payments are re-enabled.

    What alternative solutions did you explore? (Optional)

    Force reloading the browser window on payment toggle, but proper Onyx cache management is smoother and avoids full page reloads.

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 contributorOverdue

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions