Skip to content

Reuse the active workspace when adding a bank account to pay an invoice as a business - #97047

Merged
marcochavezf merged 5 commits into
Expensify:mainfrom
wildan-m:wildan/92859-invoice-add-bank-account-duplicate-workspace
Jul 29, 2026
Merged

marcochavezf merged 5 commits into
Expensify:mainfrom
wildan-m:wildan/92859-invoice-add-bank-account-duplicate-workspace

Conversation

@wildan-m

@wildan-m wildan-m commented Jul 26, 2026 •

Copy link
Copy Markdown
Contributor

Explanation of Change

When paying an invoice as a business, the "Add bank account" option resolves which workspace the bank account should be attached to. For an individual invoice receiver it reuses the active workspace only when that workspace is one the user administers and its currency is already supported for direct reimbursement — and only US dollars qualify. A payer whose workspace is in any other currency therefore fails the reuse check on every attempt, so a brand new workspace is minted each time and the account fills up with duplicates. The first press makes matters worse rather than better: the workspace it creates takes the payer's local currency, becomes the active one, and then sends the user into the currency setup screen, so the very next press hits the same failing check again and loops.

The currency check does not belong in the reuse-or-create decision. Whether a workspace is already in a supported currency governs whether direct reimbursement can be offered, not whether an in-progress workspace should be abandoned — the screen the user lands on next exists precisely to finish configuring it. Basing the decision on administration rights alone is strictly broader than the previous condition, since the old one already implied it, so every case that reused a workspace before still reuses the same one; only the looping path changes. Visibility of the option is untouched and continues to use the currency check.

Fixed Issues

$ #92859
PROPOSAL: #92859 (comment)

Tests

Preconditions

  1. The payer's account has the payInvoiceViaExpensify beta. If you are running the branch locally without it, override Permissions.canUseAllBetas to return true — the approach documented in CONTRIBUTING.md § Working on beta features. Revert before pushing.
  2. Sender: on the sending workspace, turn on More features → Earn → Invoices, and set Overview → Default currency → USD. The invoice inherits the workspace's currency, so a non-USD workspace hides the payment options entirely no matter which currency you pick on the amount screen.
  3. Sender: send an invoice to the payer as an individual.
  4. Payer: set Account → Preferences → Payment currency → USD, otherwise "Add bank account" is not offered.
  5. Payer: create a workspace (creating it makes you its admin), set its Overview → Default currency to any non-USD currency, then Workspaces → ⋮ → Set as default workspace.

Steps

  1. Open the unpaid invoice, click the dropdown next to the Pay button, and select Pay as a business → Add bank account.
  2. Verify it opens the bank account setup for the workspace from precondition 5 — the route keeps that workspace's policyID — and that no new workspace was created (open Workspaces and confirm the count is unchanged).
  3. Go back to the invoice and repeat step 1 twice more. Verify the same policyID is used every time and the workspace count never grows.
  4. Regression check: set a workspace you administer whose currency is USD as your default workspace, then repeat step 1. Verify it still opens that workspace's bank account setup, exactly as before this change.

On main, step 1 creates a brand new workspace (named <workspace> 1, <workspace> 2, and so on) and every repeat creates another one.

Offline tests

Same as tests. This change only alters which existing workspace ID is chosen before navigating, so there is no new network request and no new offline behavior. Reusing a workspace avoids the workspace-creation write entirely, so the offline path performs strictly less optimistic work than before.

QA Steps

Same as tests, with one requirement that cannot be worked around on staging: the payer's account must have the payInvoiceViaExpensify beta enabled. Without it the "Pay as a business" submenu has no bank account options at all.

  • 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 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 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)
  • 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)
  • 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.
  • 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: Nativehttps://github.com/user-attachments/assets/6afe40a8-7bc2-4d2f-878f-b8d9a3bb7e84

summary>

Android: mWeb Chrome
Kapture.2026-07-27.at.21.52.54.mp4
iOS: Native
Kapture.2026-07-27.at.21.42.08.mp4
iOS: mWeb Safari
Kapture.2026-07-27.at.21.34.50.mp4
MacOS: Chrome / Safari
Kapture.2026-07-27.at.21.22.38.mp4

@wildan-m
wildan-m marked this pull request as ready for review July 27, 2026 14:54
@wildan-m
wildan-m requested review from a team as code owners July 27, 2026 14:54
@melvin-bot
melvin-bot Bot requested review from JmillsExpensify and brunovjk and removed request for a team July 27, 2026 14:54
@melvin-bot

melvin-bot Bot commented Jul 27, 2026

Copy link
Copy Markdown

@brunovjk 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 removed the request for review from a team July 27, 2026 14:55

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 670c7673f1

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +423 to 424
if (hasActivePolicyAsAdmin) {
return activePolicy.id;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep bank setup currency aligned with the invoice

When the active admin workspace is EUR but the invoice and payer currency are USD—the exact scenario added to the regression test—this branch now opens bank setup for the EUR workspace. The non-USD setup fixes the account currency to policy.outputCurrency (NonUSDVerifiedBankAccountFlowPage.tsx lines 74–76), while SettlementButton only offers accounts whose bankCurrency matches the invoice currency via matchesCurrency() (PaymentUtils.ts lines 165–166). The newly created EUR account therefore never appears as a payment method for the USD invoice, leaving the payer unable to complete payment; reuse needs to route the workspace through a currency change first or otherwise preserve creation of a currency-compatible workspace.

Useful? React with 👍 / 👎.

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.

Verified this in the app — the mechanism is real. With a EUR active workspace the reuse branch does enter the non-USD flow, so the new account's currency follows the workspace rather than the invoice.

It isn't a regression though. In that same state (payer local currency IDR, USD invoice, EUR active workspace), main doesn't reuse, so it creates a workspace in the payer's local currency — IDR — which isn't globally reimbursable and dead-ends on the "set your workspace currency to USD" screen. And it does that on every press, which is the duplicate-workspace bug being fixed here. Neither path produces a payable account in that scenario; main just also litters workspaces.

main only lands on a compatible currency when the payer's local currency happens to match the invoice, since it comes from localCurrencyCode and is independent of the invoice currency. So bank-account currency vs invoice currency is a pre-existing gap rather than something this change introduces, and it's worth tracking separately — making reuse currency-aware here would mean reinstating the exact gate that causes the duplicates.

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.

Thanks for checking this @wildan-m. What do you think about the specific case where the invoice and the payer’s payment currency are USD, while the active workspace is EUR?
I believe this may differ from the IDR scenario mentioned in your response. Could the resulting EUR bank account be filtered out when returning to the USD invoice? I’m not completely certain, so perhaps it would be worth verifying this exact scenario. What do you think?

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.

@brunovjk Yes, it'd be filtered — matchesCurrency compares bankCurrency against the invoice currency. Whether that's a regression turns on the payer's localCurrencyCode rather than their payment currency: if it's non-USD (what I tested) main also ends up unpayable and spawns a workspace per press, so only a USD localCurrencyCode favours main — and that one I can't reproduce, since it's set at signup with no setting to change it.

@JmillsExpensify JmillsExpensify 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.

LGTM

@brunovjk

brunovjk commented Jul 28, 2026 •

Copy link
Copy Markdown
Contributor

Reviewer Checklist

  • I have verified the author checklist is complete (all boxes are checked off).
  • I verified the correct issue is linked in the ### Fixed Issues section above
  • I verified testing steps are clear and they cover the changes made in this PR
    • I verified the steps for local testing are in the Tests section
    • I verified the steps for Staging and/or Production testing are in the QA steps section
    • I verified the steps cover any possible 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 checked that screenshots or videos are included for tests on all platforms
  • I included screenshots or videos for tests on all platforms
  • I verified that the composer does not automatically focus or open the keyboard on mobile unless explicitly intended. This includes checking that returning the app from the background does not unexpectedly open the keyboard.
  • I verified tests pass on all platforms & I tested again on:
    • Android: HybridApp
    • Android: mWeb Chrome
    • iOS: HybridApp
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • If there are any errors in the console that are unrelated to this PR, I either fixed them (preferred) or linked to where I reported them in Slack
  • I verified proper code patterns were followed (see Reviewing the code)
    • 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 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)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I verified that this PR follows the guidelines as stated in the Review Guidelines
  • I verified other components that can be impacted by these changes have been tested, and I retested again (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar have been tested & I retested again)
  • If a new component is created I verified that:
    • A similar component doesn't exist in the codebase
    • All props are defined accurately
    • The component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
    • The only data being stored in the state is data necessary for rendering and nothing else
    • The component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions
  • 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 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.
  • For any bug fix or new feature in this PR, I verified that sufficient unit tests are included to prevent regressions in this 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.
  • I have checked off every checkbox in the PR reviewer checklist, including those that don't apply to this PR.

Screenshots/Videos

Android: HybridApp

Uploading 97047_android_native.mov…

Android: mWeb Chrome

Uploading 97047_android_web.mov…

iOS: HybridApp

Uploading 97047_ios_native.mov…

iOS: mWeb Safari

Uploading 97047_ios_web.mov…

MacOS: Chrome / Safari
97047_web_chrome.mov

@melvin-bot
melvin-bot Bot requested a review from marcochavezf July 28, 2026 14:00
@brunovjk

Copy link
Copy Markdown
Contributor

All yours @marcochavezf. Thanks.

@marcochavezf
marcochavezf merged commit 17eb5c9 into Expensify:main Jul 29, 2026
39 of 46 checks passed
@OSBotify

Copy link
Copy Markdown
Contributor

✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release.

@github-actions

Copy link
Copy Markdown
Contributor

🚧 marcochavezf has triggered a test Expensify/App build. You can view the workflow run here.

@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/marcochavezf in version: 9.4.46-0 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

@MelvinBot

Copy link
Copy Markdown
Contributor

No help site changes are required for this PR, so I did not create a draft docs PR.

Why: This is a bug fix to internal logic. The change in src/components/SettlementButton/index.tsx only alters which existing workspace ID is chosen before navigating to bank-account setup (reusing the active workspace instead of minting duplicates). It does not change any user-facing step, feature name, tab, setting, or button label.

I reviewed the relevant help article, Pay-an-invoice.md, which documents the payer flow (Pay → pay as an individual / as a business → Add Bank Account). That flow is unchanged by this PR — the fix corrects the duplicate-workspace bug behind the scenes, and the duplicate behavior was never documented. So the article already reflects the corrected behavior; nothing needs to be added or edited.

@wildan-m, if you believe there's a documented behavior I missed that this PR changes, let me know and I'll draft the help site update.

@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to production by https://github.com/marcaaron in version: 9.4.46-10 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

Bundle Size Analysis (Sentry):

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