From 4824e96e9be1398b3330e07224aadb9720f83f36 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Mon, 2 Feb 2026 11:15:13 +0100 Subject: [PATCH 01/46] feat: add bankAccountID to OpenReimbursementAccountPageParams --- src/libs/API/parameters/OpenReimbursementAccountPageParams.ts | 3 ++- src/libs/actions/BankAccounts.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/libs/API/parameters/OpenReimbursementAccountPageParams.ts b/src/libs/API/parameters/OpenReimbursementAccountPageParams.ts index 8e4ae5208a2e..496a53836d85 100644 --- a/src/libs/API/parameters/OpenReimbursementAccountPageParams.ts +++ b/src/libs/API/parameters/OpenReimbursementAccountPageParams.ts @@ -7,7 +7,8 @@ type OpenReimbursementAccountPageParams = { stepToOpen: ReimbursementAccountStep; subStep: ReimbursementAccountSubStep; localCurrentStep: ReimbursementAccountStep; - policyID: string; + policyID?: string; + bankAccountID?: number; }; export default OpenReimbursementAccountPageParams; diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 2875cdbc4391..d1cc62ca2269 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -1013,7 +1013,7 @@ function clearReimbursementAccountSendReminderForCorpaySignerInformation() { * @param localCurrentStep - last step on device * @param policyID - policy ID */ -function openReimbursementAccountPage(stepToOpen: ReimbursementAccountStep, subStep: ReimbursementAccountSubStep, localCurrentStep: ReimbursementAccountStep, policyID: string) { +function openReimbursementAccountPage(stepToOpen: ReimbursementAccountStep, subStep: ReimbursementAccountSubStep, localCurrentStep: ReimbursementAccountStep, policyID?: string, bankAccountID?: number) { const onyxData: OnyxData = { optimisticData: [ { @@ -1049,6 +1049,7 @@ function openReimbursementAccountPage(stepToOpen: ReimbursementAccountStep, subS subStep, localCurrentStep, policyID, + bankAccountID, }; return API.read(READ_COMMANDS.OPEN_REIMBURSEMENT_ACCOUNT_PAGE, parameters, onyxData); From 7344c2f7db1f17d4a18cc5f2de2fa08673c61b5b Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Mon, 2 Feb 2026 11:17:48 +0100 Subject: [PATCH 02/46] feat: accept bankAccountID as a param --- src/ROUTES.ts | 24 ++++++++++++++++--- src/components/KYCWall/BaseKYCWall.tsx | 6 ++--- src/components/Navigation/DebugTabView.tsx | 8 +++---- .../SearchPageHeader/SearchFiltersBar.tsx | 2 +- src/components/SettlementButton/index.tsx | 3 ++- src/libs/ReportUtils.ts | 3 ++- .../ReimbursementAccount/navigation.ts | 2 +- .../NonUSD/Finish/index.tsx | 2 +- .../ReimbursementAccountVerifyAccountPage.tsx | 4 ++-- .../components/Enable2FACard.tsx | 2 +- src/pages/Search/SearchSelectedNarrow.tsx | 9 ++++++- .../WorkspaceExpensifyCardBankAccounts.tsx | 8 ++++++- .../WorkspaceExpensifyCardPageEmptyState.tsx | 8 ++++++- 13 files changed, 60 insertions(+), 21 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index f391d08d0254..b48c45ad6ca7 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -185,9 +185,27 @@ const ROUTES = { BANK_ACCOUNT_PERSONAL: 'bank-account/personal', BANK_ACCOUNT_WITH_STEP_TO_OPEN: { route: 'bank-account/:stepToOpen?', - getRoute: (policyID: string | undefined, stepToOpen: ReimbursementAccountStepToOpen = '', backTo?: string, subStepToOpen?: typeof CONST.BANK_ACCOUNT.STEP.COUNTRY) => { - if (!policyID) { - Log.warn('Invalid policyID is used to build the BANK_ACCOUNT_WITH_STEP_TO_OPEN route'); + getRoute: ({ + policyID, + stepToOpen = '', + bankAccountID, + backTo, + subStepToOpen, + }: { + policyID: string | undefined; + stepToOpen?: ReimbursementAccountStepToOpen; + bankAccountID?: number; + backTo?: string; + subStepToOpen?: typeof CONST.BANK_ACCOUNT.STEP.COUNTRY; + }) => { + if (!policyID && !bankAccountID) { + // eslint-disable-next-line no-restricted-syntax -- Legacy route generation + Log.warn('Invalid policyID or bankAccountID is used to build the BANK_ACCOUNT_WITH_STEP_TO_OPEN route'); + } + + if (bankAccountID) { + // eslint-disable-next-line no-restricted-syntax -- Legacy route generation + return getUrlWithBackToParam(`bank-account/${stepToOpen}?bankAccountID=${bankAccountID}`, backTo); } // TODO this backTo comes from drilling it through bank account form screens // should be removed once https://github.com/Expensify/App/pull/72219 is resolved diff --git a/src/components/KYCWall/BaseKYCWall.tsx b/src/components/KYCWall/BaseKYCWall.tsx index ecad604578c0..a342910b98ba 100644 --- a/src/components/KYCWall/BaseKYCWall.tsx +++ b/src/components/KYCWall/BaseKYCWall.tsx @@ -147,7 +147,7 @@ function KYCWall({ if (adminPolicy?.achAccount) { return; } - Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(adminPolicy.id)); + Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute({policyID: adminPolicy.id})); }); } else { const moveResult = moveIOUReportToPolicy(iouReport, adminPolicy, true, reportTransactions, isCustomReportNamesBetaEnabled); @@ -159,7 +159,7 @@ function KYCWall({ if (adminPolicy?.achAccount) { return; } - Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(adminPolicy.id)); + Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute({policyID: adminPolicy.id})); }); } } @@ -177,7 +177,7 @@ function KYCWall({ } // Navigate to the bank account set up flow for this specific policy - Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(policyID)); + Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute({policyID})); return; } diff --git a/src/components/Navigation/DebugTabView.tsx b/src/components/Navigation/DebugTabView.tsx index 304dca6145c6..48e82ef7e169 100644 --- a/src/components/Navigation/DebugTabView.tsx +++ b/src/components/Navigation/DebugTabView.tsx @@ -80,10 +80,10 @@ function getSettingsRoute(status: IndicatorStatus | undefined, reimbursementAcco case CONST.INDICATOR_STATUS.HAS_POLICY_ERRORS: return ROUTES.WORKSPACE_INITIAL.getRoute(policyIDWithErrors); case CONST.INDICATOR_STATUS.HAS_REIMBURSEMENT_ACCOUNT_ERRORS: - return ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute( - reimbursementAccount?.achData?.policyID, - getReimbursementAccountRouteForCurrentStep(reimbursementAccount?.achData?.currentStep ?? CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT), - ); + return ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute({ + policyID: reimbursementAccount?.achData?.policyID, + stepToOpen: getReimbursementAccountRouteForCurrentStep(reimbursementAccount?.achData?.currentStep ?? CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT), + }); case CONST.INDICATOR_STATUS.HAS_SUBSCRIPTION_ERRORS: return ROUTES.SETTINGS_SUBSCRIPTION.route; case CONST.INDICATOR_STATUS.HAS_SUBSCRIPTION_INFO: diff --git a/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx b/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx index 7e34fcdc3462..f055bf0392c6 100644 --- a/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx +++ b/src/components/Search/SearchPageHeader/SearchFiltersBar.tsx @@ -890,7 +890,7 @@ function SearchFiltersBar({ enablePaymentsRoute={ROUTES.ENABLE_PAYMENTS} iouReport={selectedIOUReport} addBankAccountRoute={ - isCurrentSelectedExpenseReport ? ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(currentSelectedPolicyID, undefined, Navigation.getActiveRoute()) : undefined + isCurrentSelectedExpenseReport ? ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute({policyID: currentSelectedPolicyID, backTo: Navigation.getActiveRoute()}) : undefined } onSuccessfulKYC={(paymentType) => confirmPayment?.(paymentType)} > diff --git a/src/components/SettlementButton/index.tsx b/src/components/SettlementButton/index.tsx index 56bb9b8d3cf0..2964628bd2cd 100644 --- a/src/components/SettlementButton/index.tsx +++ b/src/components/SettlementButton/index.tsx @@ -54,6 +54,7 @@ type KYCFlowEvent = GestureResponderEvent | KeyboardEvent | undefined; type TriggerKYCFlow = (params: ContinueActionParams) => void; type CurrencyType = TupleToUnion; + function SettlementButton({ addDebitCardRoute = ROUTES.IOU_SEND_ADD_DEBIT_CARD, kycWallAnchorAlignment = { @@ -606,7 +607,7 @@ function SettlementButton({ isDisabled={isOffline} source={CONST.KYC_WALL_SOURCE.REPORT} chatReportID={chatReportID} - addBankAccountRoute={isExpenseReport ? ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(iouReport?.policyID, undefined, Navigation.getActiveRoute()) : undefined} + addBankAccountRoute={isExpenseReport ? ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute({policyID: iouReport?.policyID, backTo: Navigation.getActiveRoute()}) : undefined} iouReport={iouReport} policy={lastPaymentPolicy} anchorAlignment={kycWallAnchorAlignment} diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 7246c21c56ca..a5e12bdc47f2 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1808,7 +1808,7 @@ function isPublicAnnounceRoom(report: OnyxEntry): boolean { */ function getBankAccountRoute(report: OnyxEntry): Route { if (isPolicyExpenseChat(report)) { - return ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(report?.policyID, undefined, Navigation.getActiveRoute()); + return ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute({policyID: report?.policyID, backTo: Navigation.getActiveRoute()}); } if (isInvoiceRoom(report) && report?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS) { @@ -11019,6 +11019,7 @@ function getOptimisticDataForParentReportAction(report: Report | undefined, last }; }); } + /** * Get optimistic data of the ancestor report actions * @param ancestors The thread report ancestors diff --git a/src/libs/actions/ReimbursementAccount/navigation.ts b/src/libs/actions/ReimbursementAccount/navigation.ts index 9ee7c295a8d7..c1dcdfa19d1f 100644 --- a/src/libs/actions/ReimbursementAccount/navigation.ts +++ b/src/libs/actions/ReimbursementAccount/navigation.ts @@ -19,7 +19,7 @@ function goToWithdrawalAccountSetupStep(stepID: BankAccountStep) { * @param [backTo] - An optional return path. If provided, it will be URL-encoded and appended to the resulting URL. */ function navigateToBankAccountRoute(policyID: string | undefined, backTo?: string, navigationOptions?: LinkToOptions) { - Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(policyID, '', backTo), navigationOptions); + Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute({policyID, backTo}), navigationOptions); } export {goToWithdrawalAccountSetupStep, navigateToBankAccountRoute}; diff --git a/src/pages/ReimbursementAccount/NonUSD/Finish/index.tsx b/src/pages/ReimbursementAccount/NonUSD/Finish/index.tsx index 7c1ce44b8290..e6f4692f5628 100644 --- a/src/pages/ReimbursementAccount/NonUSD/Finish/index.tsx +++ b/src/pages/ReimbursementAccount/NonUSD/Finish/index.tsx @@ -68,7 +68,7 @@ function Finish() { { title: translate('finishStep.secure'), onPress: () => { - Navigation.navigate(ROUTES.SETTINGS_2FA_ROOT.getRoute(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(policyID))); + Navigation.navigate(ROUTES.SETTINGS_2FA_ROOT.getRoute(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute({policyID}))); }, icon: icons.Shield, shouldShowRightIcon: true, diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountVerifyAccountPage.tsx b/src/pages/ReimbursementAccount/ReimbursementAccountVerifyAccountPage.tsx index b3aa944dd39d..819f5c75c041 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountVerifyAccountPage.tsx +++ b/src/pages/ReimbursementAccount/ReimbursementAccountVerifyAccountPage.tsx @@ -13,9 +13,9 @@ function ReimbursementAccountVerifyAccountPage({route}: ReimbursementAccountVeri const {policyID, backTo} = route.params; return ( { - Navigation.goBack(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(policyID, '', backTo, CONST.BANK_ACCOUNT.STEP.COUNTRY), {compareParams: false}); + Navigation.goBack(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute({policyID, backTo, subStepToOpen: CONST.BANK_ACCOUNT.STEP.COUNTRY}), {compareParams: false}); }} /> ); diff --git a/src/pages/ReimbursementAccount/USD/ConnectBankAccount/components/Enable2FACard.tsx b/src/pages/ReimbursementAccount/USD/ConnectBankAccount/components/Enable2FACard.tsx index f3f95c5e08a1..125b47eee146 100644 --- a/src/pages/ReimbursementAccount/USD/ConnectBankAccount/components/Enable2FACard.tsx +++ b/src/pages/ReimbursementAccount/USD/ConnectBankAccount/components/Enable2FACard.tsx @@ -32,7 +32,7 @@ function Enable2FACard({policyID}: Enable2FACardProps) { { title: translate('connectBankAccountStep.secureYourAccount'), // Assuming user is validated here, validation is checked at the beginning of ConnectBank Flow - onPress: () => Navigation.navigate(ROUTES.SETTINGS_2FA_ROOT.getRoute(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(policyID))), + onPress: () => Navigation.navigate(ROUTES.SETTINGS_2FA_ROOT.getRoute(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute({policyID}))), icon: Shield, shouldShowRightIcon: true, outerWrapperStyle: shouldUseNarrowLayout ? styles.mhn5 : styles.mhn8, diff --git a/src/pages/Search/SearchSelectedNarrow.tsx b/src/pages/Search/SearchSelectedNarrow.tsx index 9ad0c8a16c7e..a5e376f4b4a0 100644 --- a/src/pages/Search/SearchSelectedNarrow.tsx +++ b/src/pages/Search/SearchSelectedNarrow.tsx @@ -62,7 +62,14 @@ function SearchSelectedNarrow({options, itemsLength, currentSelectedPolicyID, cu chatReportID={currentSelectedReportID} iouReport={selectedIouReport} enablePaymentsRoute={ROUTES.ENABLE_PAYMENTS} - addBankAccountRoute={isCurrentSelectedExpenseReport ? ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(currentSelectedPolicyID, undefined, Navigation.getActiveRoute()) : undefined} + addBankAccountRoute={ + isCurrentSelectedExpenseReport + ? ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute({ + policyID: currentSelectedPolicyID, + backTo: Navigation.getActiveRoute(), + }) + : undefined + } onSuccessfulKYC={(paymentType) => confirmPayment?.(paymentType)} > {(triggerKYCFlow, buttonRef) => ( diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardBankAccounts.tsx b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardBankAccounts.tsx index b581d5702424..c711b1590df4 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardBankAccounts.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardBankAccounts.tsx @@ -70,7 +70,13 @@ function WorkspaceExpensifyCardBankAccounts({route}: WorkspaceExpensifyCardBankA }; const handleAddBankAccount = () => { - Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(policyID, REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.NEW, ROUTES.WORKSPACE_EXPENSIFY_CARD.getRoute(policyID))); + Navigation.navigate( + ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute({ + policyID, + stepToOpen: REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.NEW, + backTo: ROUTES.WORKSPACE_EXPENSIFY_CARD.getRoute(policyID), + }), + ); }; const handleSelectBankAccount = (value?: number) => { diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPageEmptyState.tsx b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPageEmptyState.tsx index 6ec72aab94e9..f2a20478555d 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPageEmptyState.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPageEmptyState.tsx @@ -54,7 +54,13 @@ function WorkspaceExpensifyCardPageEmptyState({route, policy}: WorkspaceExpensif const startFlow = useCallback(() => { if (!eligibleBankAccounts.length || isSetupUnfinished) { - Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(policy?.id, REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.NEW, ROUTES.WORKSPACE_EXPENSIFY_CARD.getRoute(policy?.id))); + Navigation.navigate( + ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute({ + policyID: policy?.id, + stepToOpen: REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.NEW, + backTo: ROUTES.WORKSPACE_EXPENSIFY_CARD.getRoute(policy?.id), + }), + ); } else { Navigation.navigate(ROUTES.WORKSPACE_EXPENSIFY_CARD_BANK_ACCOUNT.getRoute(policy?.id)); } From 4a5a2c7e136a2d809159e5f17a55b63b7b2b7bdd Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Mon, 2 Feb 2026 11:27:44 +0100 Subject: [PATCH 03/46] feat: use bankAccountID instead of policyID when policyID is missing --- src/components/KYCWall/BaseKYCWall.tsx | 2 +- src/components/SettlementButton/index.tsx | 2 +- src/libs/Navigation/types.ts | 1 + .../actions/ReimbursementAccount/navigation.ts | 18 +++++++++++++++--- .../ReimbursementAccountPage.tsx | 10 ++++++++-- src/pages/settings/Wallet/WalletPage/index.tsx | 13 ++++++++----- .../ConnectExistingBusinessBankAccountPage.tsx | 4 ++-- .../WorkspaceOverviewCurrencyPage.tsx | 2 +- .../invoices/WorkspaceInvoiceVBASection.tsx | 2 +- .../workflows/WorkspaceWorkflowsPage.tsx | 4 ++-- 10 files changed, 40 insertions(+), 18 deletions(-) diff --git a/src/components/KYCWall/BaseKYCWall.tsx b/src/components/KYCWall/BaseKYCWall.tsx index a342910b98ba..ae5c041c1e05 100644 --- a/src/components/KYCWall/BaseKYCWall.tsx +++ b/src/components/KYCWall/BaseKYCWall.tsx @@ -186,7 +186,7 @@ function KYCWall({ // - account already present on policy is partially setup // - account is being connected 'on the spot' while trying to pay for an expense (it won't be linked to policy yet but will appear as reimbursementAccount) if (policy !== undefined && (isBankAccountPartiallySetup(policy?.achAccount?.state) || isBankAccountPartiallySetup(reimbursementAccount?.achData?.state))) { - navigateToBankAccountRoute(policy.id); + navigateToBankAccountRoute({policyID: policy.id}); return; } diff --git a/src/components/SettlementButton/index.tsx b/src/components/SettlementButton/index.tsx index 2964628bd2cd..e4794bc4ec4b 100644 --- a/src/components/SettlementButton/index.tsx +++ b/src/components/SettlementButton/index.tsx @@ -371,7 +371,7 @@ function SettlementButton({ icon: icons.Bank, onSelected: () => { if (payAsBusiness) { - navigateToBankAccountRoute(getPolicyID()); + navigateToBankAccountRoute({policyID: getPolicyID()}); } else { Navigation.navigate(ROUTES.SETTINGS_ADD_BANK_ACCOUNT.route); } diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index a29fb057fbda..65c2403b7f8c 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -2181,6 +2181,7 @@ type ReimbursementAccountNavigatorParamList = { // eslint-disable-next-line no-restricted-syntax -- `backTo` usages in this file are legacy. Do not add new `backTo` params to screens. See contributingGuides/NAVIGATION.md backTo?: Routes; policyID?: string; + bankAccountID?: string; subStep?: typeof CONST.BANK_ACCOUNT.STEP.COUNTRY; }; [SCREENS.REIMBURSEMENT_ACCOUNT_VERIFY_ACCOUNT]: { diff --git a/src/libs/actions/ReimbursementAccount/navigation.ts b/src/libs/actions/ReimbursementAccount/navigation.ts index c1dcdfa19d1f..7904a01fefdb 100644 --- a/src/libs/actions/ReimbursementAccount/navigation.ts +++ b/src/libs/actions/ReimbursementAccount/navigation.ts @@ -15,11 +15,23 @@ function goToWithdrawalAccountSetupStep(stepID: BankAccountStep) { /** * Navigate to the correct bank account route based on the bank account state and type * - * @param policyID - The policy ID associated with the bank account. + * @param [policyID] - The policy ID associated with the bank account. * @param [backTo] - An optional return path. If provided, it will be URL-encoded and appended to the resulting URL. + * @param [bankAccountID] - An optional bank account ID. If provided, it will be included in the resulting URL. + * @param [navigationOptions] - Optional navigation options to customize the navigation behavior. */ -function navigateToBankAccountRoute(policyID: string | undefined, backTo?: string, navigationOptions?: LinkToOptions) { - Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute({policyID, backTo}), navigationOptions); +function navigateToBankAccountRoute({ + policyID = '', + bankAccountID, + backTo, + navigationOptions, +}: { + policyID?: string; + bankAccountID?: number; + backTo?: string; + navigationOptions?: LinkToOptions; +}) { + Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute({policyID, bankAccountID, backTo}), navigationOptions); } export {goToWithdrawalAccountSetupStep, navigateToBankAccountRoute}; diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx index 39bebe2feaf7..1d731d0146c3 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx @@ -78,6 +78,7 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy, navigation}: const {isBetaEnabled} = usePermissions(); const policyName = policy?.name ?? ''; const policyIDParam = route.params?.policyID; + const bankAccountIDParam = route.params?.bankAccountID; const subStepParam = route.params?.subStep; const backTo = route.params.backTo; const isComingFromExpensifyCard = (backTo as string)?.includes(CONST.EXPENSIFY_CARD.ROUTE as string); @@ -179,6 +180,9 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy, navigation}: * Retrieve verified business bank account currently being set up. */ function fetchData(preserveCurrentStep = false) { + if (!policyIDParam && !bankAccountIDParam) { + return; + } // We can specify a step to navigate to by using route params when the component mounts. // We want to use the same stepToOpen variable when the network state changes because we can be redirected to a different step when the account refreshes. const stepToOpen = preserveCurrentStep ? currentStep : getStepToOpenFromRouteParams(route, hasConfirmedUSDCurrency); @@ -191,9 +195,11 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy, navigation}: localCurrentStep = achData?.currentStep ?? ''; } - if (policyIDParam) { - openReimbursementAccountPage(stepToOpen, subStep, localCurrentStep, policyIDParam); + if (bankAccountIDParam) { + openReimbursementAccountPage(stepToOpen, subStep, localCurrentStep, undefined, Number(bankAccountIDParam)); + return; } + openReimbursementAccountPage(stepToOpen, subStep, localCurrentStep, policyIDParam); } useEffect(() => { diff --git a/src/pages/settings/Wallet/WalletPage/index.tsx b/src/pages/settings/Wallet/WalletPage/index.tsx index c065e508a60e..332f0ab022d3 100644 --- a/src/pages/settings/Wallet/WalletPage/index.tsx +++ b/src/pages/settings/Wallet/WalletPage/index.tsx @@ -161,14 +161,17 @@ function WalletPage() { const onBankAccountRowPressed = ({accountData}: PaymentMethodPressHandlerParams) => { const accountPolicyID = accountData?.additionalData?.policyID; + const bankAccountID = accountData?.bankAccountID; + if (accountPolicyID && isAccountLocked) { + showLockedAccountModal(); + return; + } if (accountPolicyID) { - if (isAccountLocked) { - showLockedAccountModal(); - return; - } - navigateToBankAccountRoute(accountPolicyID, ROUTES.SETTINGS_WALLET); + navigateToBankAccountRoute({policyID: accountPolicyID, backTo: ROUTES.SETTINGS_WALLET}); + return; } + navigateToBankAccountRoute({bankAccountID, backTo: ROUTES.SETTINGS_WALLET}); }; const assignedCardPressed = ({event, cardData, icon, cardID}: CardPressHandlerParams) => { diff --git a/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx b/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx index a34bc943557d..a5f6ecf4e349 100644 --- a/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx +++ b/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx @@ -35,7 +35,7 @@ function ConnectExistingBusinessBankAccountPage({route}: ConnectExistingBusiness const {translate} = useLocalize(); const handleAddBankAccountPress = () => { - navigateToBankAccountRoute(policyID); + navigateToBankAccountRoute({policyID}); }; const handleItemPress = ({methodID, accountData}: PaymentMethodPressHandlerParams) => { @@ -54,7 +54,7 @@ function ConnectExistingBusinessBankAccountPage({route}: ConnectExistingBusiness Navigation.setNavigationActionToMicrotaskQueue(() => { if (isBankAccountPartiallySetup(accountData?.state)) { - navigateToBankAccountRoute(route.params.policyID); + navigateToBankAccountRoute({policyID: route.params.policyID}); } else { Navigation.closeRHPFlow(); } diff --git a/src/pages/workspace/WorkspaceOverviewCurrencyPage.tsx b/src/pages/workspace/WorkspaceOverviewCurrencyPage.tsx index e1b3ff550fdb..6da29990d9ae 100644 --- a/src/pages/workspace/WorkspaceOverviewCurrencyPage.tsx +++ b/src/pages/workspace/WorkspaceOverviewCurrencyPage.tsx @@ -54,7 +54,7 @@ function WorkspaceOverviewCurrencyPage({policy}: WorkspaceOverviewCurrencyPagePr Navigation.navigate(ROUTES.BANK_ACCOUNT_CONNECT_EXISTING_BUSINESS_BANK_ACCOUNT.getRoute(policy.id)); return; } - navigateToBankAccountRoute(policy.id, ROUTES.WORKSPACE_WORKFLOWS.getRoute(policy.id), {forceReplace: true}); + navigateToBankAccountRoute({policyID: policy.id, backTo: ROUTES.WORKSPACE_WORKFLOWS.getRoute(policy.id), navigationOptions: {forceReplace: true}}); return; } } diff --git a/src/pages/workspace/invoices/WorkspaceInvoiceVBASection.tsx b/src/pages/workspace/invoices/WorkspaceInvoiceVBASection.tsx index f7a6f72841d6..b6607cf5fe4a 100644 --- a/src/pages/workspace/invoices/WorkspaceInvoiceVBASection.tsx +++ b/src/pages/workspace/invoices/WorkspaceInvoiceVBASection.tsx @@ -183,7 +183,7 @@ function WorkspaceInvoiceVBASection({policyID}: WorkspaceInvoiceVBASectionProps) Navigation.navigate(ROUTES.BANK_ACCOUNT_CONNECT_EXISTING_BUSINESS_BANK_ACCOUNT.getRoute(policyID)); return; } - navigateToBankAccountRoute(policyID, ROUTES.WORKSPACE_INVOICES.getRoute(policyID)); + navigateToBankAccountRoute({policyID, backTo: ROUTES.WORKSPACE_INVOICES.getRoute(policyID)}); }; return ( diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx index 6fd96001330a..08aece6487e0 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx @@ -338,7 +338,7 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) { showLockedAccountModal(); return; } - navigateToBankAccountRoute(route.params.policyID, ROUTES.WORKSPACE_WORKFLOWS.getRoute(route.params.policyID)); + navigateToBankAccountRoute({policyID: route.params.policyID, backTo: ROUTES.WORKSPACE_WORKFLOWS.getRoute(route.params.policyID)}); }} displayInDefaultIconColor icon={bankIcon.icon} @@ -372,7 +372,7 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) { Navigation.navigate(ROUTES.BANK_ACCOUNT_CONNECT_EXISTING_BUSINESS_BANK_ACCOUNT.getRoute(route.params.policyID)); return; } - navigateToBankAccountRoute(route.params.policyID, ROUTES.WORKSPACE_WORKFLOWS.getRoute(route.params.policyID)); + navigateToBankAccountRoute({policyID: route.params.policyID, backTo: ROUTES.WORKSPACE_WORKFLOWS.getRoute(route.params.policyID)}); }} icon={expensifyIcons.Plus} iconHeight={20} From de98da90cec980ae90514f6f92b5772976fc386b Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 6 Feb 2026 13:56:05 +0100 Subject: [PATCH 04/46] fix: minor changes --- src/libs/actions/BankAccounts.ts | 19 ++-- .../ReimbursementAccountPage.tsx | 87 ++++++++++--------- src/types/form/ReimbursementAccountForm.ts | 2 +- src/types/onyx/ReimbursementAccount.ts | 3 + 4 files changed, 64 insertions(+), 47 deletions(-) diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 6c1ce6944c04..17dc6cd26f0e 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -1006,20 +1006,25 @@ function clearReimbursementAccountSendReminderForCorpaySignerInformation() { Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {isSuccess: null, isSendingReminderForCorpaySignerInformation: null}); } +type OpenReimbursementAccountPageActionParams = { + stepToOpen?: ReimbursementAccountStep; + subStep?: ReimbursementAccountSubStep; + localCurrentStep?: ReimbursementAccountStep; + policyID?: string; + bankAccountID?: number; + shouldPreserveDraft?: boolean; +}; + /** * Function to display and fetch data for Reimbursement Account step * @param stepToOpen - current step to open * @param subStep - particular step * @param localCurrentStep - last step on device * @param policyID - policy ID + * @param bankAccountID - bank account ID + * @param shouldPreserveDraft - if the draft should be preserved */ -function openReimbursementAccountPage( - stepToOpen: ReimbursementAccountStep, - subStep: ReimbursementAccountSubStep, - localCurrentStep: ReimbursementAccountStep, - policyID: string, - shouldPreserveDraft?: boolean, -) { +function openReimbursementAccountPage({stepToOpen = '', subStep = '', localCurrentStep = '', policyID, bankAccountID, shouldPreserveDraft}: OpenReimbursementAccountPageActionParams) { const onyxData: OnyxData = { optimisticData: [ { diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx index a1ac1037ce35..b34709988d12 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx @@ -1,5 +1,6 @@ import {useIsFocused} from '@react-navigation/native'; import {Str} from 'expensify-common'; +import {deepEqual} from 'fast-equals'; import lodashPick from 'lodash/pick'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; @@ -65,6 +66,15 @@ import VerifiedBankAccountFlowEntryPoint from './VerifiedBankAccountFlowEntryPoi type ReimbursementAccountPageProps = WithPolicyOnyxProps & PlatformStackScreenProps; type CurrencyType = TupleToUnion; +const OFFLINE_ACCESSIBLE_STEPS = [ + CONST.BANK_ACCOUNT.STEP.COUNTRY, + CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT, + CONST.BANK_ACCOUNT.STEP.COMPANY, + CONST.BANK_ACCOUNT.STEP.REQUESTOR, + CONST.BANK_ACCOUNT.STEP.BENEFICIAL_OWNERS, + CONST.BANK_ACCOUNT.STEP.ACH_CONTRACT, +] as const; + function ReimbursementAccountPage({route, policy, isLoadingPolicy, navigation}: ReimbursementAccountPageProps) { const {environmentURL} = useEnvironment(); const session = useSession(); @@ -88,7 +98,8 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy, navigation}: const requestorStepRef = useRef(null); const prevReimbursementAccount = usePrevious(reimbursementAccount); const prevIsOffline = usePrevious(isOffline); - const policyCurrency = policy?.outputCurrency ?? ''; + const achData = reimbursementAccount?.achData; + const policyCurrency = policy ? policy.outputCurrency : (achData?.currency ?? reimbursementAccountDraft?.currency); const prevPolicyCurrency = usePrevious(policyCurrency); const achContractValuesRef = useRef<{ isAuthorizedToUseBankAccount?: boolean; @@ -96,11 +107,12 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy, navigation}: acceptTermsAndConditions?: boolean; }>({}); const isLoadingWorkspaceReimbursement = policy?.isLoadingWorkspaceReimbursement; - const isNonUSDWorkspace = policyCurrency !== CONST.CURRENCY.USD; + const isNonUSDWorkspace = !!policyCurrency && policyCurrency !== CONST.CURRENCY.USD; const hasUnsupportedCurrency = isComingFromExpensifyCard && isBetaEnabled(CONST.BETAS.EXPENSIFY_CARD_EU_UK) && isNonUSDWorkspace ? !isCurrencySupportedForECards(policyCurrency) - : !isCurrencySupportedForGlobalReimbursement(policyCurrency as CurrencyType); + : policyCurrency && !isCurrencySupportedForGlobalReimbursement(policyCurrency as CurrencyType); + const nonUSDCountryDraftValue = reimbursementAccountDraft?.country ?? ''; let workspaceRoute = ''; const isFocused = useIsFocused(); @@ -115,9 +127,8 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy, navigation}: }, []); const contactMethodRoute = `${environmentURL}/${ROUTES.SETTINGS_CONTACT_METHODS.getRoute(backTo)}`; - const achData = reimbursementAccount?.achData; const isPreviousPolicy = - !!reimbursementAccount && !isLoadingOnyxValue(reimbursementAccountMetadata) ? policyIDParam === achData?.policyID : isLoadingOnyxValue(reimbursementAccountMetadata); + policyIDParam && !!reimbursementAccount && !isLoadingOnyxValue(reimbursementAccountMetadata) ? policyIDParam === achData?.policyID : isLoadingOnyxValue(reimbursementAccountMetadata); const hasConfirmedUSDCurrency = (reimbursementAccountDraft?.[INPUT_IDS.ADDITIONAL_DATA.COUNTRY] ?? '') !== '' || (achData?.accountNumber ?? '') !== ''; const isDocusignStepRequired = requiresDocusignStep(policyCurrency); @@ -138,6 +149,14 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy, navigation}: const [nonUSDBankAccountStep, setNonUSDBankAccountStep] = useState(subStepParam ?? null); const [USDBankAccountStep, setUSDBankAccountStep] = useState(subStepParam ?? null); const [isResettingBankAccount, setIsResettingBankAccount] = useState(false); + const [isNonUSDSetup, setIsNonUSDSetup] = useState(policy ? isNonUSDWorkspace : achData?.currency !== CONST.CURRENCY.USD || reimbursementAccountDraft?.currency !== CONST.CURRENCY.USD); + + useEffect(() => { + if (!policyCurrency || isNonUSDSetup === (policyCurrency !== CONST.CURRENCY.USD)) { + return; + } + setIsNonUSDSetup(policyCurrency !== CONST.CURRENCY.USD); + }, [policyCurrency, isNonUSDSetup]); useEffect(() => { const achContractValues = lodashPick(reimbursementAccountDraft, ['isAuthorizedToUseBankAccount', 'certifyTrueInformation', 'acceptTermsAndConditions']); @@ -162,9 +181,11 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy, navigation}: } const shouldShowContinueSetupButtonValue = useMemo(() => { - return hasInProgressVBBA(achData, isNonUSDWorkspace, nonUSDCountryDraftValue); - }, [achData, isNonUSDWorkspace, nonUSDCountryDraftValue]); + return hasInProgressVBBA(achData, isNonUSDSetup); + }, [achData, isNonUSDSetup]); + const isDefaultReimbursementAccountData = deepEqual(reimbursementAccount, CONST.REIMBURSEMENT_ACCOUNT.DEFAULT_DATA); + const hasLoadedData = reimbursementAccount?.achData && !isDefaultReimbursementAccountData && reimbursementAccount?.isLoading === false; /** When this page is first opened, `reimbursementAccount` prop might not yet be fully loaded from Onyx. Calculating `shouldShowContinueSetupButton` immediately on initial render doesn't make sense as @@ -172,7 +193,7 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy, navigation}: the full `reimbursementAccount` data from the server. This logic is handled within the useEffect hook, which acts similarly to `componentDidUpdate` when the `reimbursementAccount` dependency changes. */ - const [hasACHDataBeenLoaded, setHasACHDataBeenLoaded] = useState(reimbursementAccount !== CONST.REIMBURSEMENT_ACCOUNT.DEFAULT_DATA && isPreviousPolicy); + const [hasACHDataBeenLoaded, setHasACHDataBeenLoaded] = useState(hasLoadedData); const [shouldShowContinueSetupButton, setShouldShowContinueSetupButton] = useState(shouldShowContinueSetupButtonValue); const [shouldShowConnectedVerifiedBankAccount, setShouldShowConnectedVerifiedBankAccount] = useState(false); @@ -195,11 +216,9 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy, navigation}: localCurrentStep = achData?.currentStep ?? ''; } - if (bankAccountIDParam) { - openReimbursementAccountPage(stepToOpen, subStep, localCurrentStep, undefined, Number(bankAccountIDParam)); - return; - } - openReimbursementAccountPage(stepToOpen, subStep, localCurrentStep, policyIDParam, preserveCurrentStep); + // When preserving the current step (e.g., coming back online), also preserve the draft + // to prevent losing user selections made while offline + openReimbursementAccountPage({stepToOpen, subStep, localCurrentStep, policyID: policyIDParam, shouldPreserveDraft: preserveCurrentStep}); } useEffect(() => { @@ -223,7 +242,7 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy, navigation}: }, [isPreviousPolicy]); // Only re-run this effect when isPreviousPolicy changes, which happens once when the component first loads useEffect(() => { - if (!isPreviousPolicy) { + if (policyIDParam && !isPreviousPolicy) { return; } @@ -235,20 +254,20 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy, navigation}: // Sync USDBankAccountStep state with achData.currentStep when backend data changes. // This keeps state updated for legitimate step transitions while preventing flicker during transient re-renders. - if (!isNonUSDWorkspace && USDBankAccountStep !== null && achData?.currentStep && achData.currentStep !== USDBankAccountStep) { + if (!isNonUSDSetup && USDBankAccountStep !== null && achData?.currentStep && achData.currentStep !== USDBankAccountStep) { setUSDBankAccountStep(achData.currentStep); } - setShouldShowConnectedVerifiedBankAccount(isNonUSDWorkspace ? achData?.state === CONST.BANK_ACCOUNT.STATE.OPEN : achData?.currentStep === CONST.BANK_ACCOUNT.STEP.ENABLE); + setShouldShowConnectedVerifiedBankAccount(isNonUSDSetup ? achData?.state === CONST.BANK_ACCOUNT.STATE.OPEN : achData?.currentStep === CONST.BANK_ACCOUNT.STEP.ENABLE); setShouldShowContinueSetupButton(shouldShowContinueSetupButtonValue); - }, [achData?.currentStep, shouldShowContinueSetupButtonValue, isNonUSDWorkspace, isPreviousPolicy, achData?.state, policyCurrency, USDBankAccountStep]); + }, [policyIDParam, achData?.currentStep, shouldShowContinueSetupButtonValue, isNonUSDSetup, isPreviousPolicy, achData?.state, policyCurrency, USDBankAccountStep]); useEffect(() => { if (!prevPolicyCurrency || policyCurrency === prevPolicyCurrency) { return; } - if (policyCurrency === CONST.CURRENCY.USD) { + if (policyCurrency && policyCurrency === CONST.CURRENCY.USD) { setNonUSDBankAccountStep(null); } else { setUSDBankAccountStep(null); @@ -264,7 +283,7 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy, navigation}: } if (!hasACHDataBeenLoaded) { - if (reimbursementAccount !== CONST.REIMBURSEMENT_ACCOUNT.DEFAULT_DATA && reimbursementAccount?.isLoading === false) { + if (hasLoadedData) { setHasACHDataBeenLoaded(true); } return; @@ -311,7 +330,7 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy, navigation}: navigation.setParams({stepToOpen}); }, // eslint-disable-next-line react-hooks/exhaustive-deps - [isOffline, reimbursementAccount, hasACHDataBeenLoaded, shouldShowContinueSetupButton, currentStep], + [isOffline, reimbursementAccount?.draftStep, reimbursementAccount?.pendingAction, hasACHDataBeenLoaded, shouldShowContinueSetupButton, currentStep], ); const continueUSDVBBASetup = useCallback(() => { @@ -437,17 +456,7 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy, navigation}: (isLoadingApp || (reimbursementAccount?.isLoading && !reimbursementAccount?.isCreateCorpayBankAccount)) && (!plaidCurrentEvent || plaidCurrentEvent === CONST.BANK_ACCOUNT.PLAID.EVENTS_NAME.EXIT); - const shouldShowOfflineLoader = !( - isOffline && - [ - CONST.BANK_ACCOUNT.STEP.COUNTRY, - CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT, - CONST.BANK_ACCOUNT.STEP.COMPANY, - CONST.BANK_ACCOUNT.STEP.REQUESTOR, - CONST.BANK_ACCOUNT.STEP.BENEFICIAL_OWNERS, - CONST.BANK_ACCOUNT.STEP.ACH_CONTRACT, - ].some((value) => value === currentStep) - ); + const shouldShowOfflineLoader = !(hasLoadedData && isOffline && OFFLINE_ACCESSIBLE_STEPS.some((value) => value === currentStep)); const shouldShowPolicyName = topmostFullScreenRoute?.name === NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR; const policyNameToDisplay = shouldShowPolicyName ? policyName : ''; @@ -459,11 +468,11 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy, navigation}: // Show loading indicator when page is first time being opened and props.reimbursementAccount yet to be loaded from the server // or when data is being loaded. Don't show the loading indicator if we're offline and restarted the bank account setup process // On Android, when we open the app from the background, Onfido activity gets destroyed, so we need to reopen it. - if ((!hasACHDataBeenLoaded || isLoading || isLoadingWorkspaceReimbursement) && shouldShowOfflineLoader && (shouldReopenOnfido || !requestorStepRef?.current)) { + if ((isLoading || isLoadingWorkspaceReimbursement) && shouldShowOfflineLoader && (shouldReopenOnfido || !requestorStepRef?.current)) { return ; } - if ((!isLoading && (isEmptyObject(policy) || !isPolicyAdmin(policy))) || isPendingDeletePolicy(policy)) { + if (!!policyIDParam && ((!isLoading && (isEmptyObject(policy) || !isPolicyAdmin(policy))) || isPendingDeletePolicy(policy))) { return ( ); } - if (isNonUSDWorkspace && nonUSDBankAccountStep !== null && !isResettingBankAccount) { + if (isNonUSDSetup && nonUSDBankAccountStep !== null && !isResettingBankAccount) { return ( ); } - if (!isNonUSDWorkspace && USDBankAccountStep !== null) { + if (!isNonUSDSetup && USDBankAccountStep !== null) { return ( ; + > & {currency?: string}; export type { ReimbursementAccountForm, diff --git a/src/types/onyx/ReimbursementAccount.ts b/src/types/onyx/ReimbursementAccount.ts index 730c2f9c6f1a..4b1117578a47 100644 --- a/src/types/onyx/ReimbursementAccount.ts +++ b/src/types/onyx/ReimbursementAccount.ts @@ -196,6 +196,9 @@ type ACHData = Partial Date: Fri, 6 Feb 2026 14:17:13 +0100 Subject: [PATCH 05/46] feat: 79607 Change bank account --- src/languages/en.ts | 1 + .../ConnectedVerifiedBankAccount.tsx | 20 ++++++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index fee76443fc33..751e66fcfcee 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -6026,6 +6026,7 @@ const translations = { workspaceCurrencyNotSupported: 'Workspace currency not supported', yourWorkspace: `Your workspace is set to an unsupported currency. View the list of supported currencies.`, chooseAnExisting: 'Choose an existing bank account to pay expenses or add a new one.', + changeBankAccount: 'Change bank account', }, changeOwner: { changeOwnerPageTitle: 'Transfer owner', diff --git a/src/pages/ReimbursementAccount/ConnectedVerifiedBankAccount.tsx b/src/pages/ReimbursementAccount/ConnectedVerifiedBankAccount.tsx index a6f8fde701df..1a5fec542734 100644 --- a/src/pages/ReimbursementAccount/ConnectedVerifiedBankAccount.tsx +++ b/src/pages/ReimbursementAccount/ConnectedVerifiedBankAccount.tsx @@ -2,7 +2,7 @@ import React from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import getBankIcon from '@components/Icon/BankIcons'; -import {Close} from '@components/Icon/Expensicons'; +import {Bank, Close} from '@components/Icon/Expensicons'; import {loadIllustration} from '@components/Icon/IllustrationLoader'; import type {IllustrationName} from '@components/Icon/IllustrationLoader'; import MenuItem from '@components/MenuItem'; @@ -15,8 +15,10 @@ import {useMemoizedLazyAsset} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@navigation/Navigation'; import WorkspaceResetBankAccountModal from '@pages/workspace/WorkspaceResetBankAccountModal'; import {requestResetBankAccount, resetReimbursementAccount} from '@userActions/ReimbursementAccount'; +import ROUTES from '@src/ROUTES'; import type {ReimbursementAccount} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -62,6 +64,14 @@ function ConnectedVerifiedBankAccount({ const pendingAction = reimbursementAccount?.pendingAction; const shouldShowResetModal = reimbursementAccount?.shouldShowResetModal ?? false; const {asset: ThumbsUpStars} = useMemoizedLazyAsset(() => loadIllustration('ThumbsUpStars' as IllustrationName)); + const policyID = reimbursementAccount?.achData?.policyID; + const navigateToLinkExistingBankAccounts = () => { + if (!policyID) { + return; + } + + Navigation.navigate(ROUTES.BANK_ACCOUNT_CONNECT_EXISTING_BUSINESS_BANK_ACCOUNT.getRoute(policyID)); + }; return ( {translate('workspace.bankAccount.accountDescriptionWithCards')} + Date: Mon, 9 Feb 2026 13:10:20 +0100 Subject: [PATCH 06/46] pause --- .../ReimbursementAccountPage.tsx | 13 +++++++++++++ .../ConnectExistingBusinessBankAccountPage.tsx | 8 +++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx index b34709988d12..99b20762c22a 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx @@ -226,6 +226,19 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy, navigation}: return; } + // If account was just cleared (was not null, now is null), don't fetch yet + // Route params may be stale during this transition + if (!!prevReimbursementAccount && !reimbursementAccount) { + clearReimbursementAccountDraft(); + + const isStepToOpenEmpty = getStepToOpenFromRouteParams(route, hasConfirmedUSDCurrency) === ''; + if (isStepToOpenEmpty) { + setBankAccountSubStep(null); + setPlaidEvent(null); + } + return; + } + if (policyIDParam) { setReimbursementAccountLoading(true); } diff --git a/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx b/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx index a11cbc86713f..701a613867de 100644 --- a/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx +++ b/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx @@ -14,6 +14,7 @@ import type {PlatformStackScreenProps} from '@navigation/PlatformStackNavigation import type {ConnectExistingBankAccountNavigatorParamList} from '@navigation/types'; import PaymentMethodList from '@pages/settings/Wallet/PaymentMethodList'; import type {PaymentMethodPressHandlerParams} from '@pages/settings/Wallet/WalletPage/types'; +import {clearReimbursementAccount} from '@userActions/BankAccounts'; import {setWorkspaceReimbursement} from '@userActions/Policy/Policy'; import {navigateToBankAccountRoute} from '@userActions/ReimbursementAccount'; import CONST from '@src/CONST'; @@ -30,12 +31,17 @@ function ConnectExistingBusinessBankAccountPage({route}: ConnectExistingBusiness const policyName = policy?.name ?? ''; const policyCurrency = policy?.outputCurrency ?? ''; const {shouldUseNarrowLayout} = useResponsiveLayout(); + const hasFullyWorkingConnectedAccount = policy?.achAccount?.state === CONST.BANK_ACCOUNT.STATE.OPEN; const styles = useThemeStyles(); const {translate} = useLocalize(); const handleAddBankAccountPress = () => { - navigateToBankAccountRoute({policyID}); + if (hasFullyWorkingConnectedAccount) { + clearReimbursementAccount(); + } + + navigateToBankAccountRoute({policyID: hasFullyWorkingConnectedAccount ? undefined : policyID}); }; const handleItemPress = ({methodID, accountData}: PaymentMethodPressHandlerParams) => { From 37ca31124d245d46028845bb620a79ca1a0b4eb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Muzyk?= Date: Tue, 10 Feb 2026 12:52:45 +0100 Subject: [PATCH 07/46] feat: add new acc via change --- src/ONYXKEYS.ts | 4 ++++ .../actions/ReimbursementAccount/index.ts | 10 ++++++++ .../ReimbursementAccountPage.tsx | 23 +++++++++---------- .../settings/Wallet/PaymentMethodList.tsx | 9 ++++++-- ...ConnectExistingBusinessBankAccountPage.tsx | 17 +++++++++++--- 5 files changed, 46 insertions(+), 17 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 26e6118c6b3e..889512a36e27 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -349,6 +349,9 @@ const ONYXKEYS = { /** Stores information about the active reimbursement account being set up */ REIMBURSEMENT_ACCOUNT: 'reimbursementAccount', + /** Indicates whether the user started changing their business bank account to a fresh account */ + IS_CHANGING_TO_NEW_BANK_ACCOUNT: 'isChangingToNewBankAccount', + /** Stores Workspace ID that will be tied to reimbursement account during setup */ REIMBURSEMENT_ACCOUNT_WORKSPACE_ID: 'reimbursementAccountWorkspaceID', @@ -1305,6 +1308,7 @@ type OnyxValuesMapping = { [ONYXKEYS.SHARE_BANK_ACCOUNT]: OnyxTypes.ShareBankAccount; [ONYXKEYS.UNSHARE_BANK_ACCOUNT]: OnyxTypes.UnshareBankAccount; [ONYXKEYS.REIMBURSEMENT_ACCOUNT]: OnyxTypes.ReimbursementAccount; + [ONYXKEYS.IS_CHANGING_TO_NEW_BANK_ACCOUNT]: boolean; [ONYXKEYS.REIMBURSEMENT_ACCOUNT_OPTION_PRESSED]: ValueOf; [ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE]: number; [ONYXKEYS.FREQUENTLY_USED_EMOJIS]: OnyxTypes.FrequentlyUsedEmoji[]; diff --git a/src/libs/actions/ReimbursementAccount/index.ts b/src/libs/actions/ReimbursementAccount/index.ts index d7d216d699f7..7fb8aab3703c 100644 --- a/src/libs/actions/ReimbursementAccount/index.ts +++ b/src/libs/actions/ReimbursementAccount/index.ts @@ -45,6 +45,14 @@ function clearReimbursementAccount() { Onyx.set(ONYXKEYS.REIMBURSEMENT_ACCOUNT, CONST.REIMBURSEMENT_ACCOUNT.DEFAULT_DATA); } +function setIsChangingToNewBankAccount() { + Onyx.set(ONYXKEYS.IS_CHANGING_TO_NEW_BANK_ACCOUNT, true); +} + +function cancelChangingToNewBankAccount() { + Onyx.set(ONYXKEYS.IS_CHANGING_TO_NEW_BANK_ACCOUNT, false); +} + /** * Triggers a modal to open allowing the user to reset their bank account */ @@ -79,4 +87,6 @@ export { setBankAccountState, setReimbursementAccountOptionPressed, updateReimbursementAccount, + setIsChangingToNewBankAccount, + cancelChangingToNewBankAccount, }; diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx index 42868164e313..c37bb58a81bb 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx @@ -44,7 +44,7 @@ import { } from '@userActions/BankAccounts'; import {getPaymentMethods} from '@userActions/PaymentMethods'; import {isCurrencySupportedForGlobalReimbursement} from '@userActions/Policy/Policy'; -import {clearReimbursementAccount, clearReimbursementAccountDraft} from '@userActions/ReimbursementAccount'; +import {cancelChangingToNewBankAccount, clearReimbursementAccount, clearReimbursementAccountDraft} from '@userActions/ReimbursementAccount'; import CONST from '@src/CONST'; import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -84,6 +84,7 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy, navigation}: const [onfidoToken = ''] = useOnyx(ONYXKEYS.ONFIDO_TOKEN, {canBeMissing: true}); const [isLoadingApp = false] = useOnyx(ONYXKEYS.IS_LOADING_APP, {canBeMissing: true}); const topmostFullScreenRoute = useRootNavigationState((state) => state?.routes.findLast((lastRoute) => isFullScreenName(lastRoute.name))); + const [isChangingBusinessBankAccount] = useOnyx(ONYXKEYS.IS_CHANGING_TO_NEW_BANK_ACCOUNT, {canBeMissing: true}); const {isBetaEnabled} = usePermissions(); const policyName = policy?.name ?? ''; @@ -155,6 +156,9 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy, navigation}: clearReimbursementAccountDraft(); clearReimbursementAccount(); } + + // When user closes RHP, we want to make sure that he is not in the changing account flow anymore so data can be refetched + cancelChangingToNewBankAccount(); getPaymentMethods(true); }; // eslint-disable-next-line react-hooks/exhaustive-deps @@ -210,7 +214,11 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy, navigation}: * Retrieve verified business bank account currently being set up. */ function fetchData(preserveCurrentStep = false) { - if ((!policyIDParam && !bankAccountIDParam) || isLoadingOnyxValue(reimbursementAccountMetadata)) { + if ( + (!policyIDParam && !bankAccountIDParam) || + isLoadingOnyxValue(reimbursementAccountMetadata) || + (policyIDParam !== undefined && backTo === ROUTES.BANK_ACCOUNT_CONNECT_EXISTING_BUSINESS_BANK_ACCOUNT.getRoute(policyIDParam)) + ) { return; } if (bankAccountIDParam) { @@ -240,16 +248,7 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy, navigation}: return; } - // If account was just cleared (was not null, now is null), don't fetch yet - // Route params may be stale during this transition - if (!!prevReimbursementAccount && !reimbursementAccount) { - clearReimbursementAccountDraft(); - - const isStepToOpenEmpty = getStepToOpenFromRouteParams(route, hasConfirmedUSDCurrency) === ''; - if (isStepToOpenEmpty) { - setBankAccountSubStep(null); - setPlaidEvent(null); - } + if (isChangingBusinessBankAccount) { return; } diff --git a/src/pages/settings/Wallet/PaymentMethodList.tsx b/src/pages/settings/Wallet/PaymentMethodList.tsx index f88004dafdf6..81c8f7c46b64 100644 --- a/src/pages/settings/Wallet/PaymentMethodList.tsx +++ b/src/pages/settings/Wallet/PaymentMethodList.tsx @@ -109,6 +109,9 @@ type PaymentMethodListProps = { /* Currency of payment method to filter by */ filterCurrency?: string; + /* bank account ID of account that we do not want to show (ie: it's already connected) */ + excludeBankAccountID?: number; + /** Whether to show the default badge for the payment method */ shouldHideDefaultBadge?: boolean; @@ -159,6 +162,7 @@ function PaymentMethodList({ itemIconRight, filterType, filterCurrency, + excludeBankAccountID, shouldHideDefaultBadge = false, threeDotsMenuItems, onThreeDotsMenuPress, @@ -378,13 +382,14 @@ function PaymentMethodList({ ); } - if (filterType ?? filterCurrency) { + if (filterType ?? filterCurrency ?? excludeBankAccountID) { combinedPaymentMethods = combinedPaymentMethods.filter((paymentMethod) => { const account = paymentMethod as BankAccount; const typeMatches = !filterType || account.accountData?.type === filterType; const currencyMatches = !filterCurrency || account.bankCurrency === filterCurrency; + const shouldInclude = !excludeBankAccountID || account.methodID !== excludeBankAccountID; - return typeMatches && currencyMatches; + return typeMatches && currencyMatches && shouldInclude; }); } diff --git a/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx b/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx index 81cb46403a77..792cb59e14ad 100644 --- a/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx +++ b/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx @@ -9,15 +9,17 @@ import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import {isBankAccountPartiallySetup} from '@libs/BankAccountUtils'; +import mapCurrencyToCountry from '@libs/mapCurrencyToCountry'; import Navigation from '@navigation/Navigation'; import type {PlatformStackScreenProps} from '@navigation/PlatformStackNavigation/types'; import type {ConnectExistingBankAccountNavigatorParamList} from '@navigation/types'; import PaymentMethodList from '@pages/settings/Wallet/PaymentMethodList'; import type {PaymentMethodPressHandlerParams} from '@pages/settings/Wallet/WalletPage/types'; -import {clearReimbursementAccount} from '@userActions/BankAccounts'; +import {updateReimbursementAccountDraft} from '@userActions/BankAccounts'; import {setWorkspaceReimbursement} from '@userActions/Policy/Policy'; -import {navigateToBankAccountRoute} from '@userActions/ReimbursementAccount'; +import {clearReimbursementAccount, clearReimbursementAccountDraft, navigateToBankAccountRoute, setIsChangingToNewBankAccount} from '@userActions/ReimbursementAccount'; import CONST from '@src/CONST'; +import type {Country} from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; @@ -32,12 +34,20 @@ function ConnectExistingBusinessBankAccountPage({route}: ConnectExistingBusiness const policyCurrency = policy?.outputCurrency ?? ''; const {shouldUseNarrowLayout} = useResponsiveLayout(); const hasFullyWorkingConnectedAccount = policy?.achAccount?.state === CONST.BANK_ACCOUNT.STATE.OPEN; + const fullyWorkingConnectedAccountBankAccountID = policy?.achAccount?.bankAccountID ?? CONST.DEFAULT_NUMBER_ID; const styles = useThemeStyles(); const {translate} = useLocalize(); const handleAddBankAccountPress = () => { - navigateToBankAccountRoute({policyID}); + if (hasFullyWorkingConnectedAccount) { + clearReimbursementAccount(); + clearReimbursementAccountDraft(); + updateReimbursementAccountDraft({country: mapCurrencyToCountry(policyCurrency) as Country, currency: policyCurrency}); + setIsChangingToNewBankAccount(); + } + + navigateToBankAccountRoute(hasFullyWorkingConnectedAccount ? {} : {policyID}); }; const handleItemPress = ({methodID, accountData}: PaymentMethodPressHandlerParams) => { @@ -88,6 +98,7 @@ function ConnectExistingBusinessBankAccountPage({route}: ConnectExistingBusiness itemIconRight={icons.ArrowRight} filterType={CONST.BANK_ACCOUNT.TYPE.BUSINESS} filterCurrency={policyCurrency} + excludeBankAccountID={fullyWorkingConnectedAccountBankAccountID} shouldHideDefaultBadge /> From e8f259a21445fb446c33d3ef7d9acda695363928 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Muzyk?= Date: Wed, 11 Feb 2026 13:26:27 +0100 Subject: [PATCH 08/46] feat: small adjustments --- src/libs/WorkflowUtils.ts | 10 ++++++-- .../actions/ReimbursementAccount/index.ts | 20 ++++++++++++++-- .../ConnectedVerifiedBankAccount.tsx | 23 +++++++++++++++---- ...ConnectExistingBusinessBankAccountPage.tsx | 10 ++------ 4 files changed, 46 insertions(+), 17 deletions(-) diff --git a/src/libs/WorkflowUtils.ts b/src/libs/WorkflowUtils.ts index ac96cda9387b..70bcea79b838 100644 --- a/src/libs/WorkflowUtils.ts +++ b/src/libs/WorkflowUtils.ts @@ -494,7 +494,12 @@ function updateWorkflowDataOnApproverRemoval({approvalWorkflows, removedApprover /** * Get eligible business bank accounts for the workspace reimbursement workflow */ -function getEligibleExistingBusinessBankAccounts(bankAccountList: BankAccountList | undefined, policyCurrency: string | undefined, shouldIncludePartiallySetup?: boolean) { +function getEligibleExistingBusinessBankAccounts( + bankAccountList: BankAccountList | undefined, + policyCurrency: string | undefined, + shouldIncludePartiallySetup?: boolean, + excludeBankAccountID?: number, +) { if (!bankAccountList || policyCurrency === undefined) { return []; } @@ -503,7 +508,8 @@ function getEligibleExistingBusinessBankAccounts(bankAccountList: BankAccountLis return ( account.bankCurrency === policyCurrency && (account.accountData?.state === CONST.BANK_ACCOUNT.STATE.OPEN || (shouldIncludePartiallySetup && isBankAccountPartiallySetup(account.accountData?.state))) && - account.accountData?.type === CONST.BANK_ACCOUNT.TYPE.BUSINESS + account.accountData?.type === CONST.BANK_ACCOUNT.TYPE.BUSINESS && + account.methodID !== excludeBankAccountID ); }); } diff --git a/src/libs/actions/ReimbursementAccount/index.ts b/src/libs/actions/ReimbursementAccount/index.ts index 7fb8aab3703c..6714e67444e7 100644 --- a/src/libs/actions/ReimbursementAccount/index.ts +++ b/src/libs/actions/ReimbursementAccount/index.ts @@ -1,5 +1,7 @@ import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; +import mapCurrencyToCountry from '@libs/mapCurrencyToCountry'; +import type {Country} from '@src/CONST'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {ReimbursementAccountForm} from '@src/types/form'; @@ -45,10 +47,24 @@ function clearReimbursementAccount() { Onyx.set(ONYXKEYS.REIMBURSEMENT_ACCOUNT, CONST.REIMBURSEMENT_ACCOUNT.DEFAULT_DATA); } -function setIsChangingToNewBankAccount() { +/** + * Prepares the app to set up a new bank account by clearing existing data, + * initializing draft with country and currency, and marking the account change. + * We need to temporarily clear this data to set up new account without disconnecting existing one + */ +function prepareNewBankAccountSetup(currency: string) { + clearReimbursementAccount(); + clearReimbursementAccountDraft(); + updateReimbursementAccountDraft({ + country: mapCurrencyToCountry(currency) as Country, + currency, + }); Onyx.set(ONYXKEYS.IS_CHANGING_TO_NEW_BANK_ACCOUNT, true); } +/** + * Cancels the change to new bank account + */ function cancelChangingToNewBankAccount() { Onyx.set(ONYXKEYS.IS_CHANGING_TO_NEW_BANK_ACCOUNT, false); } @@ -87,6 +103,6 @@ export { setBankAccountState, setReimbursementAccountOptionPressed, updateReimbursementAccount, - setIsChangingToNewBankAccount, cancelChangingToNewBankAccount, + prepareNewBankAccountSetup, }; diff --git a/src/pages/ReimbursementAccount/ConnectedVerifiedBankAccount.tsx b/src/pages/ReimbursementAccount/ConnectedVerifiedBankAccount.tsx index 1a5fec542734..8b63cd7c1ad1 100644 --- a/src/pages/ReimbursementAccount/ConnectedVerifiedBankAccount.tsx +++ b/src/pages/ReimbursementAccount/ConnectedVerifiedBankAccount.tsx @@ -13,11 +13,14 @@ import Section from '@components/Section'; import Text from '@components/Text'; import {useMemoizedLazyAsset} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; +import {getEligibleExistingBusinessBankAccounts} from '@libs/WorkflowUtils'; import Navigation from '@navigation/Navigation'; import WorkspaceResetBankAccountModal from '@pages/workspace/WorkspaceResetBankAccountModal'; -import {requestResetBankAccount, resetReimbursementAccount} from '@userActions/ReimbursementAccount'; +import {navigateToBankAccountRoute, prepareNewBankAccountSetup, requestResetBankAccount, resetReimbursementAccount} from '@userActions/ReimbursementAccount'; +import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {ReimbursementAccount} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -65,12 +68,22 @@ function ConnectedVerifiedBankAccount({ const shouldShowResetModal = reimbursementAccount?.shouldShowResetModal ?? false; const {asset: ThumbsUpStars} = useMemoizedLazyAsset(() => loadIllustration('ThumbsUpStars' as IllustrationName)); const policyID = reimbursementAccount?.achData?.policyID; - const navigateToLinkExistingBankAccounts = () => { - if (!policyID) { + const currency = reimbursementAccount?.achData?.currency; + const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST, {canBeMissing: true}); + const hasOtherEligibleAccountsToConnect = getEligibleExistingBusinessBankAccounts(bankAccountList, currency, true, reimbursementAccount?.achData?.bankAccountID).length > 0; + + const handleChangeBankAccount = () => { + if (!policyID || !currency) { + return; + } + + if (hasOtherEligibleAccountsToConnect) { + Navigation.navigate(ROUTES.BANK_ACCOUNT_CONNECT_EXISTING_BUSINESS_BANK_ACCOUNT.getRoute(policyID)); return; } - Navigation.navigate(ROUTES.BANK_ACCOUNT_CONNECT_EXISTING_BUSINESS_BANK_ACCOUNT.getRoute(policyID)); + prepareNewBankAccountSetup(currency); + navigateToBankAccountRoute({}); }; return ( @@ -111,7 +124,7 @@ function ConnectedVerifiedBankAccount({ { if (hasFullyWorkingConnectedAccount) { - clearReimbursementAccount(); - clearReimbursementAccountDraft(); - updateReimbursementAccountDraft({country: mapCurrencyToCountry(policyCurrency) as Country, currency: policyCurrency}); - setIsChangingToNewBankAccount(); + prepareNewBankAccountSetup(policyCurrency); } navigateToBankAccountRoute(hasFullyWorkingConnectedAccount ? {} : {policyID}); From d250fbb3cdfda03710cd7bdd72066d68d973128a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Muzyk?= Date: Fri, 20 Feb 2026 12:47:45 +0100 Subject: [PATCH 09/46] fix: missing translations --- src/languages/de.ts | 1 + src/languages/es.ts | 1 + src/languages/fr.ts | 1 + src/languages/it.ts | 1 + src/languages/ja.ts | 1 + src/languages/nl.ts | 1 + src/languages/pl.ts | 1 + src/languages/pt-BR.ts | 1 + src/languages/zh-hans.ts | 1 + 9 files changed, 9 insertions(+) diff --git a/src/languages/de.ts b/src/languages/de.ts index 25b6922ce127..cc734834fcbb 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -6047,6 +6047,7 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU workspaceCurrencyNotSupported: 'Workspace-Währung wird nicht unterstützt', yourWorkspace: `Dein Arbeitsbereich ist auf eine nicht unterstützte Währung eingestellt. Sieh dir die Liste der unterstützten Währungen an.`, chooseAnExisting: 'Wähle ein bestehendes Bankkonto zum Bezahlen von Ausgaben oder füge ein neues hinzu.', + changeBankAccount: 'Bankkonto ändern', }, changeOwner: { changeOwnerPageTitle: 'Besitz übertragen', diff --git a/src/languages/es.ts b/src/languages/es.ts index 40cea4391309..b1406a0b287b 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -5862,6 +5862,7 @@ ${amount} para ${merchant} - ${date}`, workspaceCurrencyNotSupported: 'Moneda del espacio de trabajo no soportada', yourWorkspace: `Tu espacio de trabajo está configurado en una moneda no soportada. Consulta la lista de monedas soportadas.`, chooseAnExisting: 'Elige una cuenta bancaria existente para pagar gastos o añade una nueva.', + changeBankAccount: 'Cambiar cuenta bancaria', }, changeOwner: { changeOwnerPageTitle: 'Transferir la propiedad', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 9c3b2c219758..14b419e7afc9 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -6069,6 +6069,7 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST. workspaceCurrencyNotSupported: "Devise de l'espace de travail non prise en charge", yourWorkspace: `Votre espace de travail est défini sur une devise non prise en charge. Consultez la liste des devises prises en charge.`, chooseAnExisting: 'Choisissez un compte bancaire existant pour payer les dépenses ou ajoutez-en un nouveau.', + changeBankAccount: 'Changer de compte bancaire', }, changeOwner: { changeOwnerPageTitle: 'Transférer le responsable', diff --git a/src/languages/it.ts b/src/languages/it.ts index a99155d907aa..9a0f03614acc 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -6033,6 +6033,7 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST. workspaceCurrencyNotSupported: 'Valuta dello spazio di lavoro non supportata', yourWorkspace: `La tua area di lavoro è impostata su una valuta non supportata. Visualizza l’elenco delle valute supportate.`, chooseAnExisting: 'Scegli un conto bancario esistente per pagare le spese oppure aggiungine uno nuovo.', + changeBankAccount: 'Cambia conto bancario', }, changeOwner: { changeOwnerPageTitle: 'Trasferisci proprietario', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 063392543442..ac16007eb337 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -5976,6 +5976,7 @@ _詳しい手順については、[ヘルプサイトをご覧ください](${CO workspaceCurrencyNotSupported: 'ワークスペースの通貨はサポートされていません', yourWorkspace: `ご利用のワークスペースはサポートされていない通貨に設定されています。サポートされている通貨の一覧を表示します。`, chooseAnExisting: '既存の銀行口座を選択して経費を支払うか、新しい口座を追加してください。', + changeBankAccount: '銀行口座を変更', }, changeOwner: { changeOwnerPageTitle: '所有者を変更', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 9edae3fb90f5..9c84f5739541 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -6015,6 +6015,7 @@ _Voor meer gedetailleerde instructies, [bezoek onze help-site](${CONST.NETSUITE_ workspaceCurrencyNotSupported: 'Werkruimtevaluta wordt niet ondersteund', yourWorkspace: `Je werkruimte is ingesteld op een niet-ondersteunde valuta. Bekijk de lijst met ondersteunde valuta's.`, chooseAnExisting: 'Kies een bestaande bankrekening om onkosten te betalen of voeg een nieuwe toe.', + changeBankAccount: 'Bankrekening wijzigen', }, changeOwner: { changeOwnerPageTitle: 'Eigenaar overdragen', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index cdd920b81d43..a24861f6809c 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -6007,6 +6007,7 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy workspaceCurrencyNotSupported: 'Waluta przestrzeni roboczej nie jest obsługiwana', yourWorkspace: `Twoje miejsce pracy jest ustawione na nieobsługiwaną walutę. Zobacz listę obsługiwanych walut.`, chooseAnExisting: 'Wybierz istniejące konto bankowe do opłacania wydatków lub dodaj nowe.', + changeBankAccount: 'Zmień konto bankowe', }, changeOwner: { changeOwnerPageTitle: 'Przenieś właściciela', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 13c42f904c76..107797410fd4 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -6014,6 +6014,7 @@ _Para instruções mais detalhadas, [visite nossa central de ajuda](${CONST.NETS workspaceCurrencyNotSupported: 'Moeda do workspace não suportada', yourWorkspace: `Seu workspace está configurado para uma moeda não compatível. Veja a lista de moedas compatíveis.`, chooseAnExisting: 'Escolha uma conta bancária existente para pagar despesas ou adicione uma nova.', + changeBankAccount: 'Alterar conta bancária', }, changeOwner: { changeOwnerPageTitle: 'Transferir proprietário', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 2b5abe97d57f..ff8c04f240fa 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -5888,6 +5888,7 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM workspaceCurrencyNotSupported: '不支持工作区货币', yourWorkspace: `您的工作区当前使用不受支持的货币。请查看支持的货币列表。`, chooseAnExisting: '选择现有银行账户来支付报销,或添加新账户。', + changeBankAccount: '更改银行账户', }, changeOwner: { changeOwnerPageTitle: '转移所有者', From abf393fb5abc1eab5233e5ee6202a2a2d8787ac6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Muzyk?= Date: Wed, 11 Mar 2026 08:48:43 +0100 Subject: [PATCH 10/46] fix: automated checks --- .../ReimbursementAccount/ConnectedVerifiedBankAccount.tsx | 4 ++-- src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/ReimbursementAccount/ConnectedVerifiedBankAccount.tsx b/src/pages/ReimbursementAccount/ConnectedVerifiedBankAccount.tsx index 720a5350b40f..ca6cf731f183 100644 --- a/src/pages/ReimbursementAccount/ConnectedVerifiedBankAccount.tsx +++ b/src/pages/ReimbursementAccount/ConnectedVerifiedBankAccount.tsx @@ -69,7 +69,7 @@ function ConnectedVerifiedBankAccount({ const icons = useMemoizedLazyExpensifyIcons(['Close'] as const); const policyID = reimbursementAccount?.achData?.policyID; const currency = reimbursementAccount?.achData?.currency; - const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST, {canBeMissing: true}); + const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); const hasOtherEligibleAccountsToConnect = getEligibleExistingBusinessBankAccounts(bankAccountList, currency, true, reimbursementAccount?.achData?.bankAccountID).length > 0; const handleChangeBankAccount = () => { @@ -123,7 +123,7 @@ function ConnectedVerifiedBankAccount({ {translate('workspace.bankAccount.accountDescriptionWithCards')} state?.routes.findLast((lastRoute) => isFullScreenName(lastRoute.name))); - const [isChangingBusinessBankAccount] = useOnyx(ONYXKEYS.IS_CHANGING_TO_NEW_BANK_ACCOUNT, {canBeMissing: true}); + const [isChangingBusinessBankAccount] = useOnyx(ONYXKEYS.IS_CHANGING_TO_NEW_BANK_ACCOUNT); const {isBetaEnabled} = usePermissions(); const policyName = policy?.name ?? ''; From a00fcb2127b16be901e0a77172bb8ce73d15170f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Muzyk?= Date: Wed, 29 Apr 2026 14:54:02 +0200 Subject: [PATCH 11/46] fix: linter --- src/pages/settings/Wallet/PaymentMethodList.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/settings/Wallet/PaymentMethodList.tsx b/src/pages/settings/Wallet/PaymentMethodList.tsx index 794485cb17f4..1c49ec663587 100644 --- a/src/pages/settings/Wallet/PaymentMethodList.tsx +++ b/src/pages/settings/Wallet/PaymentMethodList.tsx @@ -490,12 +490,12 @@ function PaymentMethodList({ shouldShowAssignedCards, isLoadingBankAccountList, bankAccountList, - customCardNames, styles, translate, isOffline, filterType, filterCurrency, + excludeBankAccountID, excludeStates, isLoadingCardList, cardList, @@ -508,6 +508,7 @@ function PaymentMethodList({ privatePersonalDetails, shouldShowRightIcon, activePaymentMethodID, + customCardNames, expensifyIcons.LuggageWithLines, expensifyIcons.ThreeDots, actionPaymentMethodType, From 05fd5bf10f743d4097ef1e559454fa8a96484812 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Muzyk?= Date: Wed, 29 Apr 2026 15:17:03 +0200 Subject: [PATCH 12/46] fix: icon --- .../ReimbursementAccount/ConnectedVerifiedBankAccount.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/ReimbursementAccount/ConnectedVerifiedBankAccount.tsx b/src/pages/ReimbursementAccount/ConnectedVerifiedBankAccount.tsx index a26a636b59ed..cfd432f02b7c 100644 --- a/src/pages/ReimbursementAccount/ConnectedVerifiedBankAccount.tsx +++ b/src/pages/ReimbursementAccount/ConnectedVerifiedBankAccount.tsx @@ -62,7 +62,7 @@ function ConnectedVerifiedBankAccount({ const pendingAction = reimbursementAccount?.pendingAction; const shouldShowResetModal = reimbursementAccount?.shouldShowResetModal ?? false; const {asset: ThumbsUpStars} = useMemoizedLazyAsset(() => loadIllustration('ThumbsUpStars' as IllustrationName)); - const icons = useMemoizedLazyExpensifyIcons(['Close']); + const icons = useMemoizedLazyExpensifyIcons(['Bank', 'Close']); const policyID = reimbursementAccount?.achData?.policyID; const currency = reimbursementAccount?.achData?.currency; const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); @@ -119,7 +119,7 @@ function ConnectedVerifiedBankAccount({ {translate('workspace.bankAccount.accountDescriptionWithCards')} Date: Fri, 19 Jun 2026 10:20:32 +0200 Subject: [PATCH 13/46] Fix broken invoice multiple accounts flow --- src/CONST/index.ts | 3 +++ src/ROUTES.ts | 6 +++++- src/libs/Navigation/types.ts | 2 ++ .../ConnectedVerifiedBankAccount.tsx | 3 ++- .../ReimbursementAccountPage.tsx | 4 ++-- .../ConnectExistingBusinessBankAccountPage.tsx | 16 +++++++++++----- 6 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 6af591bed322..d25279a2e20e 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -765,6 +765,9 @@ const CONST = { PLAID: 'plaid', NONE: '', }, + CONNECT_EXISTING_SOURCE: { + CHANGE_BANK_ACCOUNT: 'changeBankAccount', + }, REGEX: { US_ACCOUNT_NUMBER: /^[0-9]{4,17}$/, diff --git a/src/ROUTES.ts b/src/ROUTES.ts index d9790039e6ab..5502daef1bc4 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1071,7 +1071,11 @@ const ROUTES = { BANK_ACCOUNT_CONNECT_EXISTING_BUSINESS_BANK_ACCOUNT: { route: 'bank-account/connect-existing-business-bank-account', - getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`bank-account/connect-existing-business-bank-account?policyID=${policyID}`, backTo), + getRoute: (policyID: string, backTo?: string, source?: ValueOf) => { + const baseRoute = `bank-account/connect-existing-business-bank-account?policyID=${policyID}` as const; + const routeWithSource = source ? (`${baseRoute}&source=${source}` as const) : baseRoute; + return getUrlWithBackToParam(routeWithSource, backTo); + }, }, BANK_ACCOUNT_NON_USD_SETUP: { route: 'bank-account/new/global/:page?/:subPage?/:action?', diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index b69da1026616..b69cbd3578e9 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -2402,6 +2402,8 @@ type ConnectExistingBankAccountNavigatorParamList = { policyID: string; // eslint-disable-next-line no-restricted-syntax -- backTo is a temporary param will be removed after https://github.com/Expensify/App/issues/73825 is done backTo?: Routes; + /** Identifies the entry point that opened this screen, so shared logic can branch on intent */ + source?: ValueOf; }; }; diff --git a/src/pages/ReimbursementAccount/ConnectedVerifiedBankAccount.tsx b/src/pages/ReimbursementAccount/ConnectedVerifiedBankAccount.tsx index 49dd6e4d47a8..7730341db8b2 100644 --- a/src/pages/ReimbursementAccount/ConnectedVerifiedBankAccount.tsx +++ b/src/pages/ReimbursementAccount/ConnectedVerifiedBankAccount.tsx @@ -19,6 +19,7 @@ import {getEligibleExistingBusinessBankAccounts} from '@libs/WorkflowUtils'; import Navigation from '@navigation/Navigation'; import WorkspaceResetBankAccountModal from '@pages/workspace/WorkspaceResetBankAccountModal'; import {navigateToBankAccountRoute, prepareNewBankAccountSetup, requestResetBankAccount, resetReimbursementAccount} from '@userActions/ReimbursementAccount'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {ReimbursementAccount} from '@src/types/onyx'; @@ -74,7 +75,7 @@ function ConnectedVerifiedBankAccount({ } if (hasOtherEligibleAccountsToConnect) { - Navigation.navigate(ROUTES.BANK_ACCOUNT_CONNECT_EXISTING_BUSINESS_BANK_ACCOUNT.getRoute(policyID)); + Navigation.navigate(ROUTES.BANK_ACCOUNT_CONNECT_EXISTING_BUSINESS_BANK_ACCOUNT.getRoute(policyID, undefined, CONST.BANK_ACCOUNT.CONNECT_EXISTING_SOURCE.CHANGE_BANK_ACCOUNT)); return; } diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx index 77a48f467056..a951573d686b 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx @@ -92,7 +92,7 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy}: Reimbursemen const policyIDParam = route.params?.policyID; const bankAccountIDParam = route.params?.bankAccountID; const subStepParam = route.params?.subStep; - const backTo = route.params.backTo; + const backTo = route.params?.backTo; const isComingFromExpensifyCard = (backTo as string)?.includes(CONST.EXPENSIFY_CARD.ROUTE as string); const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -238,7 +238,7 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy}: Reimbursemen useEffect(() => { // Consume this route intent only once so the response changing isPreviousPolicy does not trigger another request. - const shouldOpenNewBankAccount = route.params.stepToOpen === REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.NEW && !hasRequestedNewBankAccountRef.current; + const shouldOpenNewBankAccount = route.params?.stepToOpen === REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.NEW && !hasRequestedNewBankAccountRef.current; if ((!shouldOpenNewBankAccount && isPreviousPolicy && !!reimbursementAccount) || isLoadingOnyxValue(reimbursementAccountMetadata)) { return; } diff --git a/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx b/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx index c5826659d239..f23f51fcf7ae 100644 --- a/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx +++ b/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx @@ -36,20 +36,26 @@ function ConnectExistingBusinessBankAccountPage({route}: ConnectExistingBusiness const policyName = policy?.name ?? ''; const policyCurrency = policy?.outputCurrency ?? ''; const {shouldUseNarrowLayout} = useResponsiveLayout(); - const hasFullyWorkingConnectedAccount = policy?.achAccount?.state === CONST.BANK_ACCOUNT.STATE.OPEN; - const fullyWorkingConnectedAccountBankAccountID = policy?.achAccount?.bankAccountID ?? CONST.DEFAULT_NUMBER_ID; + const isChangingBankAccount = route.params?.source === CONST.BANK_ACCOUNT.CONNECT_EXISTING_SOURCE.CHANGE_BANK_ACCOUNT; + const connectedAccountBankAccountID = policy?.achAccount?.bankAccountID ?? CONST.DEFAULT_NUMBER_ID; const styles = useThemeStyles(); const {translate} = useLocalize(); const handleAddBankAccountPress = () => { - if (hasFullyWorkingConnectedAccount) { + if (isChangingBankAccount) { prepareNewBankAccountSetup(policyCurrency); } setReimbursementAccountLoading(true); Navigation.setNavigationActionToMicrotaskQueue(() => { - Navigation.navigate(appendParam(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(hasFullyWorkingConnectedAccount ? {} : {policyID, backTo}), 'stepToOpen', REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.NEW)); + Navigation.navigate( + appendParam( + ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(isChangingBankAccount ? {policyID: undefined} : {policyID, backTo}), + 'stepToOpen', + REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.NEW, + ), + ); }); }; @@ -110,7 +116,7 @@ function ConnectExistingBusinessBankAccountPage({route}: ConnectExistingBusiness filterType={CONST.BANK_ACCOUNT.TYPE.BUSINESS} filterCurrency={policyCurrency} excludeStates={[CONST.BANK_ACCOUNT.STATE.LOCKED]} - excludeBankAccountID={fullyWorkingConnectedAccountBankAccountID} + excludeBankAccountID={isChangingBankAccount ? connectedAccountBankAccountID : undefined} shouldHideDefaultBadge /> From 8a731504fe8cff9c7a5febaae6f2c7e4e7804a0e Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 8 Jul 2026 15:20:01 +0200 Subject: [PATCH 14/46] Fix crash --- .../USD/utils/getStepToOpenFromRouteParams.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/ReimbursementAccount/USD/utils/getStepToOpenFromRouteParams.ts b/src/pages/ReimbursementAccount/USD/utils/getStepToOpenFromRouteParams.ts index 15ec4698777c..40e50bc2f766 100644 --- a/src/pages/ReimbursementAccount/USD/utils/getStepToOpenFromRouteParams.ts +++ b/src/pages/ReimbursementAccount/USD/utils/getStepToOpenFromRouteParams.ts @@ -14,7 +14,7 @@ function getStepToOpenFromRouteParams( route: PlatformStackRouteProp, hasConfirmedUSDCurrency: boolean, ): TBankAccountStep | '' { - switch (route.params.stepToOpen) { + switch (route.params?.stepToOpen) { case REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.NEW: return hasConfirmedUSDCurrency ? CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT : CONST.BANK_ACCOUNT.STEP.COUNTRY; case REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.COMPANY: From 8899c33b93b44088cc976d62f95b8ed8b9e0bd3b Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 8 Jul 2026 16:29:50 +0200 Subject: [PATCH 15/46] Add change BA button to Almost done page --- .../VerifiedBankAccountFlowEntryPoint.tsx | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/pages/ReimbursementAccount/VerifiedBankAccountFlowEntryPoint.tsx b/src/pages/ReimbursementAccount/VerifiedBankAccountFlowEntryPoint.tsx index e5fd92d9bddf..822b0b8114d7 100644 --- a/src/pages/ReimbursementAccount/VerifiedBankAccountFlowEntryPoint.tsx +++ b/src/pages/ReimbursementAccount/VerifiedBankAccountFlowEntryPoint.tsx @@ -22,11 +22,20 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {getLatestError, getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import {hasActiveAdminWorkspaces} from '@libs/PolicyUtils'; +import {getEligibleExistingBusinessBankAccounts} from '@libs/WorkflowUtils'; import {goToWithdrawalAccountSetupStep, openPlaidView, updateReimbursementAccountDraft} from '@userActions/BankAccounts'; import {setDraftValues} from '@userActions/FormActions'; import {openExternalLink} from '@userActions/Link'; -import {requestResetBankAccount, resetReimbursementAccount, setBankAccountSubStep, setReimbursementAccountOptionPressed, updateReimbursementAccount} from '@userActions/ReimbursementAccount'; +import { + navigateToBankAccountRoute, + prepareNewBankAccountSetup, + requestResetBankAccount, + resetReimbursementAccount, + setBankAccountSubStep, + setReimbursementAccountOptionPressed, + updateReimbursementAccount, +} from '@userActions/ReimbursementAccount'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -109,6 +118,26 @@ function VerifiedBankAccountFlowEntryPoint({ const personalBankAccounts = bankAccountList ? Object.keys(bankAccountList).filter((key) => bankAccountList[key].accountType === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT) : []; + const currency = reimbursementAccount?.achData?.currency; + // The "Change bank account" option is only offered when opening a partially setup account from the Workflows > Payments section + const isComingFromWorkflowsPayments = !!policyID && backTo === ROUTES.WORKSPACE_WORKFLOWS.getRoute(policyID); + const shouldShowChangeBankAccount = shouldShowContinueSetupButton === true && isComingFromWorkflowsPayments; + const hasOtherEligibleAccountsToConnect = getEligibleExistingBusinessBankAccounts(bankAccountList, currency, true, reimbursementAccount?.achData?.bankAccountID).length > 0; + + const handleChangeBankAccount = () => { + if (!policyID || !currency) { + return; + } + + if (hasOtherEligibleAccountsToConnect) { + Navigation.navigate(ROUTES.BANK_ACCOUNT_CONNECT_EXISTING_BUSINESS_BANK_ACCOUNT.getRoute(policyID, undefined, CONST.BANK_ACCOUNT.CONNECT_EXISTING_SOURCE.CHANGE_BANK_ACCOUNT)); + return; + } + + prepareNewBankAccountSetup(currency); + navigateToBankAccountRoute({}); + }; + const removeExistingBankAccountDetails = useCallback(() => { const bankAccountData: Partial = { [bankInfoStepKeys.ROUTING_NUMBER]: '', @@ -274,6 +303,16 @@ function VerifiedBankAccountFlowEntryPoint({ outerWrapperStyle={shouldUseNarrowLayout ? styles.mhn5 : styles.mhn8} disabled={!!pendingAction || (!isEmptyObject(errors) && !reimbursementAccount?.maxAttemptsReached)} /> + {shouldShowChangeBankAccount && ( + + )} Date: Wed, 8 Jul 2026 16:58:50 +0200 Subject: [PATCH 16/46] Improve optimistic data to add partial setup BA --- src/libs/actions/Policy/Policy.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 8d1f61fec37c..f09edcb4ff43 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -151,7 +151,7 @@ import type ReportNextStepDeprecated from '@src/types/onyx/ReportNextStepDepreca import type {OnyxData} from '@src/types/onyx/Request'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import type {OnyxCollection, OnyxCollectionInputValue, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; +import type {NullishDeep, OnyxCollection, OnyxCollectionInputValue, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import type {TupleToUnion, ValueOf} from 'type-fest'; /* eslint-disable max-lines */ @@ -1200,12 +1200,13 @@ function setWorkspaceReimbursement({ ]; if (bankAccountID !== undefined && bankAccountList !== undefined) { - const optimisticBankAccountList: BankAccountList = {}; + const optimisticBankAccountList: NullishDeep = {}; if (oldBankAccountID) { optimisticBankAccountList[oldBankAccountID] = { ...optimisticBankAccountList[oldBankAccountID], accountData: { policyIDs: bankAccountList?.[oldBankAccountID]?.accountData?.policyIDs?.filter((id) => id !== policyID) ?? [], + additionalData: {policyID: null}, }, }; } @@ -1214,6 +1215,7 @@ function setWorkspaceReimbursement({ ...optimisticBankAccountList[bankAccountID], accountData: { policyIDs: [...new Set([...currentPolicyIDs, policyID])], + additionalData: {policyID}, }, }; @@ -1278,12 +1280,13 @@ function setWorkspaceReimbursement({ ]; if (bankAccountID !== undefined && bankAccountList !== undefined) { - const failureBankAccountList: BankAccountList = {}; + const failureBankAccountList: NullishDeep = {}; if (oldBankAccountID) { failureBankAccountList[oldBankAccountID] = { ...failureBankAccountList[oldBankAccountID], accountData: { policyIDs: bankAccountList?.[oldBankAccountID]?.accountData?.policyIDs ?? [], + additionalData: {policyID: bankAccountList?.[oldBankAccountID]?.accountData?.additionalData?.policyID ?? null}, }, }; } @@ -1291,6 +1294,7 @@ function setWorkspaceReimbursement({ ...failureBankAccountList[bankAccountID], accountData: { policyIDs: bankAccountList?.[bankAccountID]?.accountData?.policyIDs ?? [], + additionalData: {policyID: bankAccountList?.[bankAccountID]?.accountData?.additionalData?.policyID ?? null}, }, }; failureData.push({ From 2254a7438013191b9a8db0e6db7d51ae8cba8129 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 8 Jul 2026 17:19:23 +0200 Subject: [PATCH 17/46] Fix current partial setup BA is shown in the list of suggested BA --- .../workspace/ConnectExistingBusinessBankAccountPage.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx b/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx index a2c2e09313b8..d17ec12a242d 100644 --- a/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx +++ b/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx @@ -44,7 +44,9 @@ function ConnectExistingBusinessBankAccountPage({route}: ConnectExistingBusiness const policyCurrency = policy?.outputCurrency ?? ''; const {shouldUseNarrowLayout} = useResponsiveLayout(); const isChangingBankAccount = route.params?.source === CONST.BANK_ACCOUNT.CONNECT_EXISTING_SOURCE.CHANGE_BANK_ACCOUNT; - const connectedAccountBankAccountID = policy?.achAccount?.bankAccountID ?? CONST.DEFAULT_NUMBER_ID; + const isBankAccountFullySetup = !!policy?.achAccount && (policy.achAccount.state === CONST.BANK_ACCOUNT.STATE.OPEN || policy.achAccount.state === CONST.BANK_ACCOUNT.STATE.LOCKED); + const connectedBankAccount = Object.values(bankAccountList ?? {}).find((bankAccount) => bankAccount?.accountData?.additionalData?.policyID === policyID); + const connectedAccountBankAccountID = (isBankAccountFullySetup ? policy?.achAccount?.bankAccountID : connectedBankAccount?.methodID) ?? CONST.DEFAULT_NUMBER_ID; const styles = useThemeStyles(); const {translate} = useLocalize(); From f77574963dd4fb7927f00a957c4779571a2dae63 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Thu, 9 Jul 2026 12:12:40 +0200 Subject: [PATCH 18/46] Fix linking newly added BA --- .../ConnectedVerifiedBankAccount.tsx | 2 +- .../ReimbursementAccountPage.tsx | 1 + .../VerifiedBankAccountFlowEntryPoint.tsx | 2 +- .../ConnectExistingBusinessBankAccountPage.tsx | 14 ++++++-------- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/pages/ReimbursementAccount/ConnectedVerifiedBankAccount.tsx b/src/pages/ReimbursementAccount/ConnectedVerifiedBankAccount.tsx index 16a6d30974da..3acfb7b26f5c 100644 --- a/src/pages/ReimbursementAccount/ConnectedVerifiedBankAccount.tsx +++ b/src/pages/ReimbursementAccount/ConnectedVerifiedBankAccount.tsx @@ -86,7 +86,7 @@ function ConnectedVerifiedBankAccount({ } prepareNewBankAccountSetup(currency); - navigateToBankAccountRoute({}); + navigateToBankAccountRoute({policyID}); }; useResetBankAccountModal({ diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx index 0c32ce04c983..e2fa7500d227 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx @@ -496,6 +496,7 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy}: Reimbursemen // On Android, when we open the app from the background, Onfido activity gets destroyed, so we need to reopen it. if ( (!!policyIDParam || !!bankAccountIDParam) && + !isChangingBusinessBankAccount && (!hasACHDataBeenLoaded || isLoading || isLoadingWorkspaceReimbursement) && shouldShowOfflineLoader && (shouldReopenOnfido || !requestorStepRef?.current) diff --git a/src/pages/ReimbursementAccount/VerifiedBankAccountFlowEntryPoint.tsx b/src/pages/ReimbursementAccount/VerifiedBankAccountFlowEntryPoint.tsx index 822b0b8114d7..a3282ef2e584 100644 --- a/src/pages/ReimbursementAccount/VerifiedBankAccountFlowEntryPoint.tsx +++ b/src/pages/ReimbursementAccount/VerifiedBankAccountFlowEntryPoint.tsx @@ -135,7 +135,7 @@ function VerifiedBankAccountFlowEntryPoint({ } prepareNewBankAccountSetup(currency); - navigateToBankAccountRoute({}); + navigateToBankAccountRoute({policyID}); }; const removeExistingBankAccountDetails = useCallback(() => { diff --git a/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx b/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx index d17ec12a242d..e1dd20ed12bd 100644 --- a/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx +++ b/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx @@ -52,19 +52,17 @@ function ConnectExistingBusinessBankAccountPage({route}: ConnectExistingBusiness const {translate} = useLocalize(); const handleAddBankAccountPress = () => { + // When changing the bank account we prepare a fresh setup (clearing the current account without disconnecting it). + // We keep the policyID in the route so the newly connected account still links to the workspace's Workflows > Payments. + // We must not set the loading flag here: during the change flow the page skips fetchData, so nothing would reset it. if (isChangingBankAccount) { prepareNewBankAccountSetup(policyCurrency); + } else { + setReimbursementAccountLoading(true); } - setReimbursementAccountLoading(true); Navigation.setNavigationActionToMicrotaskQueue(() => { - Navigation.navigate( - appendParam( - ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(isChangingBankAccount ? {policyID: undefined} : {policyID, backTo}), - 'stepToOpen', - REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.NEW, - ), - ); + Navigation.navigate(appendParam(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute({policyID, backTo}), 'stepToOpen', REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.NEW)); }); }; From 0892f97cbbb4a196bc478d1abc003c4b6968b623 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Thu, 9 Jul 2026 12:52:50 +0200 Subject: [PATCH 19/46] Lint fix --- src/libs/actions/ReimbursementAccount/index.ts | 3 +-- src/libs/mapCurrencyToCountry.ts | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/ReimbursementAccount/index.ts b/src/libs/actions/ReimbursementAccount/index.ts index e21069812b0c..a0c915b1026f 100644 --- a/src/libs/actions/ReimbursementAccount/index.ts +++ b/src/libs/actions/ReimbursementAccount/index.ts @@ -1,6 +1,5 @@ import mapCurrencyToCountry from '@libs/mapCurrencyToCountry'; -import type {Country} from '@src/CONST'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {ReimbursementAccountForm} from '@src/types/form'; @@ -55,7 +54,7 @@ function prepareNewBankAccountSetup(currency: string) { clearReimbursementAccount(); clearReimbursementAccountDraft(); updateReimbursementAccountDraft({ - country: mapCurrencyToCountry(currency) as Country, + country: mapCurrencyToCountry(currency), currency, }); Onyx.set(ONYXKEYS.IS_CHANGING_TO_NEW_BANK_ACCOUNT, true); diff --git a/src/libs/mapCurrencyToCountry.ts b/src/libs/mapCurrencyToCountry.ts index f4062674c183..4794dc280ee2 100644 --- a/src/libs/mapCurrencyToCountry.ts +++ b/src/libs/mapCurrencyToCountry.ts @@ -1,6 +1,7 @@ +import type {Country} from '@src/CONST'; import CONST from '@src/CONST'; -function mapCurrencyToCountry(currency: string): string { +function mapCurrencyToCountry(currency: string): Country | '' { switch (currency) { case CONST.CURRENCY.USD: return CONST.COUNTRY.US; From 3e156343e5927710cd25f0150c5f6da83cb4129c Mon Sep 17 00:00:00 2001 From: VickyStash Date: Thu, 9 Jul 2026 12:57:24 +0200 Subject: [PATCH 20/46] Hide change bank account action for wallet accounts with no linked workspace --- .../ConnectedVerifiedBankAccount.tsx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/pages/ReimbursementAccount/ConnectedVerifiedBankAccount.tsx b/src/pages/ReimbursementAccount/ConnectedVerifiedBankAccount.tsx index 3acfb7b26f5c..15c2d6c0e2fe 100644 --- a/src/pages/ReimbursementAccount/ConnectedVerifiedBankAccount.tsx +++ b/src/pages/ReimbursementAccount/ConnectedVerifiedBankAccount.tsx @@ -74,6 +74,7 @@ function ConnectedVerifiedBankAccount({ const currency = reimbursementAccount?.achData?.currency; const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); const hasOtherEligibleAccountsToConnect = getEligibleExistingBusinessBankAccounts(bankAccountList, currency, true, reimbursementAccount?.achData?.bankAccountID).length > 0; + const shouldShowChangeBankAccount = !!policyID && !!currency; const handleChangeBankAccount = () => { if (!policyID || !currency) { @@ -131,14 +132,16 @@ function ConnectedVerifiedBankAccount({ wrapperStyle={[styles.ph0, styles.mv3, styles.h13]} /> {translate('workspace.bankAccount.accountDescriptionWithCards')} - + {shouldShowChangeBankAccount && ( + + )} Date: Thu, 9 Jul 2026 15:40:58 +0200 Subject: [PATCH 21/46] Fix navigation --- .../ConnectExistingBusinessBankAccountPage.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx b/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx index e1dd20ed12bd..6d420a79b8fd 100644 --- a/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx +++ b/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx @@ -93,12 +93,18 @@ function ConnectExistingBusinessBankAccountPage({route}: ConnectExistingBusiness } Navigation.setNavigationActionToMicrotaskQueue(() => { - if (isBankAccountPartiallySetup(accountData?.state)) { - navigateToBankAccountRoute({policyID, backTo, navigationOptions: {forceReplace: true}}); - } else { + if (!isBankAccountPartiallySetup(accountData?.state)) { setReimbursementAccountLoading(false); Navigation.closeRHPFlow(); + return; } + + if (isChangingBankAccount) { + Navigation.goBack(backTo); + return; + } + + navigateToBankAccountRoute({policyID, backTo, navigationOptions: {forceReplace: true}}); }); }; From bfa01e0314e0bbd1704f0744a68b860160e5ef61 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Thu, 9 Jul 2026 15:59:38 +0200 Subject: [PATCH 22/46] Improve check --- src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx b/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx index 6d420a79b8fd..3c3152bb6b5e 100644 --- a/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx +++ b/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx @@ -73,7 +73,7 @@ function ConnectExistingBusinessBankAccountPage({route}: ConnectExistingBusiness const newReimburserEmail = policy?.achAccount?.reimburser ?? policy?.owner ?? ''; - if (bankAccountList && methodID && !bankAccountList[methodID]?.accountData?.policyIDs?.includes(policyID)) { + if (bankAccountList && methodID && methodID !== connectedAccountBankAccountID && methodID !== policy?.achAccount?.bankAccountID) { setWorkspaceReimbursement({ policyID, currentAchAccount: policy?.achAccount, From 6e1490846fa6b22bb1d33f267cac659fbacb68fc Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 10 Jul 2026 08:15:50 +0200 Subject: [PATCH 23/46] Prevent stale frame and refetch race when switching workspace bank account --- .../ReimbursementAccountPage.tsx | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx index e2fa7500d227..83485a0c3980 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx @@ -122,6 +122,8 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy}: Reimbursemen acceptTermsAndConditions?: boolean; }>({}); const isLoadingWorkspaceReimbursement = policy?.isLoadingWorkspaceReimbursement; + const prevIsLoadingWorkspaceReimbursement = usePrevious(isLoadingWorkspaceReimbursement); + const [isSettingBA, setIsSettingBA] = useState(false); const isNonUSDWorkspace = !!policyCurrency && policyCurrency !== CONST.CURRENCY.USD; const hasUnsupportedCurrency = isComingFromExpensifyCard && isBetaEnabled(CONST.BETAS.EXPENSIFY_CARD_EU_UK) && isNonUSDWorkspace @@ -248,6 +250,28 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy}: Reimbursemen openReimbursementAccountPage({stepToOpen, subStep, localCurrentStep, policyID: policyIDParam, shouldPreserveDraft: preserveCurrentStep}); } + // When the workspace's bank account is switched, the switch request and the refetch that reloads + // the new account run one after another. We must not refetch until the switch has committed to escape race condition. + useEffect(() => { + if (isLoadingWorkspaceReimbursement && !prevIsLoadingWorkspaceReimbursement) { + setIsSettingBA(true); + } + const isSettingFinished = !isLoadingWorkspaceReimbursement && prevIsLoadingWorkspaceReimbursement; + if (!isSettingFinished) { + return; + } + fetchData(); + }, [isLoadingWorkspaceReimbursement, prevIsLoadingWorkspaceReimbursement, fetchData]); + + // Keeps the loader up while switching the workspace's bank account, bridging the gap between the switch request + // finishing and the follow-up refetch's own loading flag landing. + useEffect(() => { + if (!isSettingBA || isLoadingWorkspaceReimbursement || !reimbursementAccount?.isLoading) { + return; + } + setIsSettingBA(false); + }, [isSettingBA, isLoadingWorkspaceReimbursement, reimbursementAccount?.isLoading]); + useEffect(() => { // Consume this route intent only once so the response changing isPreviousPolicy does not trigger another request. const shouldOpenNewBankAccount = route.params?.stepToOpen === REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.NEW && !hasRequestedNewBankAccountRef.current; @@ -255,7 +279,9 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy}: Reimbursemen return; } - if (isChangingBusinessBankAccount) { + // Skip while switching the workspace's bank account: the dedicated effect above refetches once the switch + // finishes, so fetching here would race it and could load the old account. + if (isChangingBusinessBankAccount || isLoadingWorkspaceReimbursement) { return; } @@ -497,7 +523,7 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy}: Reimbursemen if ( (!!policyIDParam || !!bankAccountIDParam) && !isChangingBusinessBankAccount && - (!hasACHDataBeenLoaded || isLoading || isLoadingWorkspaceReimbursement) && + (!hasACHDataBeenLoaded || isLoading || isLoadingWorkspaceReimbursement || isSettingBA) && shouldShowOfflineLoader && (shouldReopenOnfido || !requestorStepRef?.current) ) { From 5e18c32251ed99ef71d5c4d38e3a2d054988fdce Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 10 Jul 2026 08:34:25 +0200 Subject: [PATCH 24/46] Spell fix --- src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx index 83485a0c3980..b88244b45cee 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx @@ -279,7 +279,7 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy}: Reimbursemen return; } - // Skip while switching the workspace's bank account: the dedicated effect above refetches once the switch + // Skip while switching the workspace's bank account: the dedicated effect above fetches once the switch // finishes, so fetching here would race it and could load the old account. if (isChangingBusinessBankAccount || isLoadingWorkspaceReimbursement) { return; From f028f73589c038fac21f485e5d015fbfc9d75dc5 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 10 Jul 2026 09:03:47 +0200 Subject: [PATCH 25/46] Repoint last payment method bankAccountID when changing workspace bank account --- src/libs/actions/Policy/Policy.ts | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index d7160aeb18f8..445a4bd64021 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -1240,21 +1240,30 @@ function setWorkspaceReimbursement({ ]; // We're using setWorkspaceReimbursement in several places, not all of which require updating the last used payment method. - if (!lastUsedPaymentMethod && shouldUpdateLastPaymentMethod) { + if (shouldUpdateLastPaymentMethod) { + // Keep the stored bankAccountID current so getLastPolicyBankAccountID doesn't return the old account after a + // change. Record VBBA when there's no last method yet, otherwise just repoint the bankAccountID. + const lastPaymentMethodValue = lastUsedPaymentMethod + ? { + expense: {bankAccountID}, + lastUsed: {bankAccountID}, + } + : { + expense: { + name: CONST.IOU.PAYMENT_TYPE.VBBA, + bankAccountID, + }, + lastUsed: { + name: CONST.IOU.PAYMENT_TYPE.VBBA, + bankAccountID, + }, + }; + successData?.push({ onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.NVP_LAST_PAYMENT_METHOD, value: { - [policyID]: { - expense: { - name: CONST.IOU.PAYMENT_TYPE.VBBA, - bankAccountID, - }, - lastUsed: { - name: CONST.IOU.PAYMENT_TYPE.VBBA, - bankAccountID, - }, - }, + [policyID]: lastPaymentMethodValue, }, }); } From 93aafd3081d17527713a505f8c198ace8cedbd7a Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 10 Jul 2026 09:10:22 +0200 Subject: [PATCH 26/46] Revert "Repoint last payment method bankAccountID when changing workspace bank account" This reverts commit f028f73589c038fac21f485e5d015fbfc9d75dc5. --- src/libs/actions/Policy/Policy.ts | 31 +++++++++++-------------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 445a4bd64021..d7160aeb18f8 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -1240,30 +1240,21 @@ function setWorkspaceReimbursement({ ]; // We're using setWorkspaceReimbursement in several places, not all of which require updating the last used payment method. - if (shouldUpdateLastPaymentMethod) { - // Keep the stored bankAccountID current so getLastPolicyBankAccountID doesn't return the old account after a - // change. Record VBBA when there's no last method yet, otherwise just repoint the bankAccountID. - const lastPaymentMethodValue = lastUsedPaymentMethod - ? { - expense: {bankAccountID}, - lastUsed: {bankAccountID}, - } - : { - expense: { - name: CONST.IOU.PAYMENT_TYPE.VBBA, - bankAccountID, - }, - lastUsed: { - name: CONST.IOU.PAYMENT_TYPE.VBBA, - bankAccountID, - }, - }; - + if (!lastUsedPaymentMethod && shouldUpdateLastPaymentMethod) { successData?.push({ onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.NVP_LAST_PAYMENT_METHOD, value: { - [policyID]: lastPaymentMethodValue, + [policyID]: { + expense: { + name: CONST.IOU.PAYMENT_TYPE.VBBA, + bankAccountID, + }, + lastUsed: { + name: CONST.IOU.PAYMENT_TYPE.VBBA, + bankAccountID, + }, + }, }, }); } From 75915f8c67e95a1f3c77ae225934fbf297f5e038 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 10 Jul 2026 10:13:24 +0200 Subject: [PATCH 27/46] Prevent bank account list flash before navigating on selection --- ...ConnectExistingBusinessBankAccountPage.tsx | 41 ++++++++++++------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx b/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx index 3c3152bb6b5e..05e9eae64f22 100644 --- a/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx +++ b/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx @@ -1,3 +1,4 @@ +import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; @@ -29,7 +30,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import React from 'react'; +import React, {useState} from 'react'; type ConnectExistingBusinessBankAccountPageProps = PlatformStackScreenProps; @@ -51,6 +52,8 @@ function ConnectExistingBusinessBankAccountPage({route}: ConnectExistingBusiness const styles = useThemeStyles(); const {translate} = useLocalize(); + const [isSelectingBankAccount, setIsSelectingBankAccount] = useState(false); + const handleAddBankAccountPress = () => { // When changing the bank account we prepare a fresh setup (clearing the current account without disconnecting it). // We keep the policyID in the route so the newly connected account still links to the workspace's Workflows > Payments. @@ -74,6 +77,7 @@ function ConnectExistingBusinessBankAccountPage({route}: ConnectExistingBusiness const newReimburserEmail = policy?.achAccount?.reimburser ?? policy?.owner ?? ''; if (bankAccountList && methodID && methodID !== connectedAccountBankAccountID && methodID !== policy?.achAccount?.bankAccountID) { + setIsSelectingBankAccount(true); setWorkspaceReimbursement({ policyID, currentAchAccount: policy?.achAccount, @@ -118,21 +122,28 @@ function ConnectExistingBusinessBankAccountPage({route}: ConnectExistingBusiness subtitle={policyName} onBackButtonPress={Navigation.goBack} /> - - {translate('workspace.bankAccount.chooseAnExisting')} - - + ) : ( + + {translate('workspace.bankAccount.chooseAnExisting')} + + + )} ); } From d4e9cf3f3bfdd4695619ecb800397323442735bc Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 10 Jul 2026 10:23:32 +0200 Subject: [PATCH 28/46] Prevent one-frame ConnectedVerifiedBankAccount flash on BA change --- src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx index b88244b45cee..beb806bb4721 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx @@ -570,7 +570,9 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy}: Reimbursemen ); } - if (shouldShowConnectedVerifiedBankAccount) { + const isConnectedVerifiedBankAccountData = isNonUSDSetup ? achData?.state === CONST.BANK_ACCOUNT.STATE.OPEN : achData?.currentStep === CONST.BANK_ACCOUNT.STEP.ENABLE; + + if (shouldShowConnectedVerifiedBankAccount && isConnectedVerifiedBankAccountData) { if (topmostFullScreenRoute?.name === NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR) { return ( From b4fa0e2ceceed0e260ec60653bc847b2d2efd374 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 10 Jul 2026 10:34:31 +0200 Subject: [PATCH 29/46] Fix set-state-in-effect lint by latching isSettingBA during render --- .../ReimbursementAccountPage.tsx | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx index beb806bb4721..bfd98a9c1e97 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx @@ -123,7 +123,15 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy}: Reimbursemen }>({}); const isLoadingWorkspaceReimbursement = policy?.isLoadingWorkspaceReimbursement; const prevIsLoadingWorkspaceReimbursement = usePrevious(isLoadingWorkspaceReimbursement); + const [isSettingBA, setIsSettingBA] = useState(false); + // Keeps the loader up while switching the workspace's bank account, bridging the gap between the switch request + // finishing and the follow-up refetch's own loading flag landing. + if (isLoadingWorkspaceReimbursement && !prevIsLoadingWorkspaceReimbursement && !isSettingBA) { + setIsSettingBA(true); + } else if (isSettingBA && !isLoadingWorkspaceReimbursement && reimbursementAccount?.isLoading) { + setIsSettingBA(false); + } const isNonUSDWorkspace = !!policyCurrency && policyCurrency !== CONST.CURRENCY.USD; const hasUnsupportedCurrency = isComingFromExpensifyCard && isBetaEnabled(CONST.BETAS.EXPENSIFY_CARD_EU_UK) && isNonUSDWorkspace @@ -253,9 +261,6 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy}: Reimbursemen // When the workspace's bank account is switched, the switch request and the refetch that reloads // the new account run one after another. We must not refetch until the switch has committed to escape race condition. useEffect(() => { - if (isLoadingWorkspaceReimbursement && !prevIsLoadingWorkspaceReimbursement) { - setIsSettingBA(true); - } const isSettingFinished = !isLoadingWorkspaceReimbursement && prevIsLoadingWorkspaceReimbursement; if (!isSettingFinished) { return; @@ -263,15 +268,6 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy}: Reimbursemen fetchData(); }, [isLoadingWorkspaceReimbursement, prevIsLoadingWorkspaceReimbursement, fetchData]); - // Keeps the loader up while switching the workspace's bank account, bridging the gap between the switch request - // finishing and the follow-up refetch's own loading flag landing. - useEffect(() => { - if (!isSettingBA || isLoadingWorkspaceReimbursement || !reimbursementAccount?.isLoading) { - return; - } - setIsSettingBA(false); - }, [isSettingBA, isLoadingWorkspaceReimbursement, reimbursementAccount?.isLoading]); - useEffect(() => { // Consume this route intent only once so the response changing isPreviousPolicy does not trigger another request. const shouldOpenNewBankAccount = route.params?.stepToOpen === REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.NEW && !hasRequestedNewBankAccountRef.current; From 742886831b6528e3507e63de04eb6c838626d6b7 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 10 Jul 2026 10:37:03 +0200 Subject: [PATCH 30/46] Lint fix --- src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx index bfd98a9c1e97..4b9c39a27ce4 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx @@ -266,7 +266,9 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy}: Reimbursemen return; } fetchData(); - }, [isLoadingWorkspaceReimbursement, prevIsLoadingWorkspaceReimbursement, fetchData]); + // Run only on the loading transition, not when fetchData's identity changes. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isLoadingWorkspaceReimbursement, prevIsLoadingWorkspaceReimbursement]); useEffect(() => { // Consume this route intent only once so the response changing isPreviousPolicy does not trigger another request. From 11300ccea0880416f5c2703610b1f951bea2f09e Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 10 Jul 2026 11:22:19 +0200 Subject: [PATCH 31/46] Fix stale preview --- src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx index 4b9c39a27ce4..2216f542b0bf 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx @@ -600,6 +600,9 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy}: Reimbursemen ); } + // Once fresh data has loaded, trust the live value to avoid a one-frame flash from the effect-synced state lagging achData. + const shouldShowContinueSetupButtonToDisplay = hasLoadedData ? shouldShowContinueSetupButtonValue : shouldShowContinueSetupButton; + return ( Date: Fri, 10 Jul 2026 12:24:28 +0200 Subject: [PATCH 32/46] Apply minor feedback --- src/pages/settings/Wallet/PaymentMethodList.tsx | 2 +- .../ConnectExistingBusinessBankAccountPage.tsx | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/pages/settings/Wallet/PaymentMethodList.tsx b/src/pages/settings/Wallet/PaymentMethodList.tsx index 0969a7bbc8f9..3d93e6cc5b59 100644 --- a/src/pages/settings/Wallet/PaymentMethodList.tsx +++ b/src/pages/settings/Wallet/PaymentMethodList.tsx @@ -133,7 +133,7 @@ type PaymentMethodListProps = { /** Account states to exclude from the list */ excludeStates?: Array>; - /* bank account ID of account that we do not want to show (ie: it's already connected) */ + /** Bank account ID of an account that we do not want to show (i.e. it's already connected) */ excludeBankAccountID?: number; /** Whether to show the default badge for the payment method */ diff --git a/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx b/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx index 05e9eae64f22..b0b29fdfcd06 100644 --- a/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx +++ b/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx @@ -1,4 +1,4 @@ -import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import ActivityIndicator from '@components/ActivityIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; @@ -31,6 +31,7 @@ import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import React, {useState} from 'react'; +import {View} from 'react-native'; type ConnectExistingBusinessBankAccountPageProps = PlatformStackScreenProps; @@ -123,10 +124,12 @@ function ConnectExistingBusinessBankAccountPage({route}: ConnectExistingBusiness onBackButtonPress={Navigation.goBack} /> {isSelectingBankAccount ? ( - + + + ) : ( {translate('workspace.bankAccount.chooseAnExisting')} From 7b0797b8ca34e29ddec65dd860fabc3edfd65ff6 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 10 Jul 2026 12:29:17 +0200 Subject: [PATCH 33/46] Set change flag before clearing account data --- src/libs/actions/ReimbursementAccount/index.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/ReimbursementAccount/index.ts b/src/libs/actions/ReimbursementAccount/index.ts index a0c915b1026f..610435867fb6 100644 --- a/src/libs/actions/ReimbursementAccount/index.ts +++ b/src/libs/actions/ReimbursementAccount/index.ts @@ -46,18 +46,18 @@ function clearReimbursementAccount() { } /** - * Prepares the app to set up a new bank account by clearing existing data, - * initializing draft with country and currency, and marking the account change. + * Prepares the app to set up a new bank account by marking the account change, clearing existing data, + * and initializing draft with country and currency. * We need to temporarily clear this data to set up new account without disconnecting existing one */ function prepareNewBankAccountSetup(currency: string) { + Onyx.set(ONYXKEYS.IS_CHANGING_TO_NEW_BANK_ACCOUNT, true); clearReimbursementAccount(); clearReimbursementAccountDraft(); updateReimbursementAccountDraft({ country: mapCurrencyToCountry(currency), currency, }); - Onyx.set(ONYXKEYS.IS_CHANGING_TO_NEW_BANK_ACCOUNT, true); } /** From 896ccbec244fc07dfef760ec3c8dfa319d151289 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Fri, 10 Jul 2026 12:57:51 +0200 Subject: [PATCH 34/46] Extract shared change-bank-account logic into useChangeBankAccount hook --- src/hooks/useChangeBankAccount.ts | 37 ++++++ .../ConnectedVerifiedBankAccount.tsx | 28 +---- .../VerifiedBankAccountFlowEntryPoint.tsx | 28 +---- tests/unit/hooks/useChangeBankAccount.test.ts | 116 ++++++++++++++++++ 4 files changed, 159 insertions(+), 50 deletions(-) create mode 100644 src/hooks/useChangeBankAccount.ts create mode 100644 tests/unit/hooks/useChangeBankAccount.test.ts diff --git a/src/hooks/useChangeBankAccount.ts b/src/hooks/useChangeBankAccount.ts new file mode 100644 index 000000000000..2703e79a7efe --- /dev/null +++ b/src/hooks/useChangeBankAccount.ts @@ -0,0 +1,37 @@ +import {getEligibleExistingBusinessBankAccounts} from '@libs/WorkflowUtils'; + +import Navigation from '@navigation/Navigation'; + +import {navigateToBankAccountRoute, prepareNewBankAccountSetup} from '@userActions/ReimbursementAccount'; + +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; + +import useOnyx from './useOnyx'; + +/** + * Returns a handler that changes the workspace's verified business bank account. When there are other eligible + * business bank accounts to connect it opens the existing-account picker, otherwise it starts a fresh setup. + * Shared by ConnectedVerifiedBankAccount and VerifiedBankAccountFlowEntryPoint. + */ +function useChangeBankAccount(policyID: string | undefined, currency: string | undefined, bankAccountID: number | undefined) { + const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); + + return () => { + if (!policyID || !currency) { + return; + } + + const hasOtherEligibleAccountsToConnect = getEligibleExistingBusinessBankAccounts(bankAccountList, currency, true, bankAccountID).length > 0; + if (hasOtherEligibleAccountsToConnect) { + Navigation.navigate(ROUTES.BANK_ACCOUNT_CONNECT_EXISTING_BUSINESS_BANK_ACCOUNT.getRoute(policyID, undefined, CONST.BANK_ACCOUNT.CONNECT_EXISTING_SOURCE.CHANGE_BANK_ACCOUNT)); + return; + } + + prepareNewBankAccountSetup(currency); + navigateToBankAccountRoute({policyID}); + }; +} + +export default useChangeBankAccount; diff --git a/src/pages/ReimbursementAccount/ConnectedVerifiedBankAccount.tsx b/src/pages/ReimbursementAccount/ConnectedVerifiedBankAccount.tsx index 15c2d6c0e2fe..129e56885214 100644 --- a/src/pages/ReimbursementAccount/ConnectedVerifiedBankAccount.tsx +++ b/src/pages/ReimbursementAccount/ConnectedVerifiedBankAccount.tsx @@ -9,22 +9,15 @@ import ScrollView from '@components/ScrollView'; import Section from '@components/Section'; import Text from '@components/Text'; +import useChangeBankAccount from '@hooks/useChangeBankAccount'; import {useMemoizedLazyAsset, useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; -import useOnyx from '@hooks/useOnyx'; import useResetBankAccountModal from '@hooks/useResetBankAccountModal'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; -import {getEligibleExistingBusinessBankAccounts} from '@libs/WorkflowUtils'; +import {requestResetBankAccount, resetReimbursementAccount} from '@userActions/ReimbursementAccount'; -import Navigation from '@navigation/Navigation'; - -import {navigateToBankAccountRoute, prepareNewBankAccountSetup, requestResetBankAccount, resetReimbursementAccount} from '@userActions/ReimbursementAccount'; - -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; import type {ReimbursementAccount} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -72,23 +65,8 @@ function ConnectedVerifiedBankAccount({ const icons = useMemoizedLazyExpensifyIcons(['Bank', 'Close']); const policyID = reimbursementAccount?.achData?.policyID; const currency = reimbursementAccount?.achData?.currency; - const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); - const hasOtherEligibleAccountsToConnect = getEligibleExistingBusinessBankAccounts(bankAccountList, currency, true, reimbursementAccount?.achData?.bankAccountID).length > 0; const shouldShowChangeBankAccount = !!policyID && !!currency; - - const handleChangeBankAccount = () => { - if (!policyID || !currency) { - return; - } - - if (hasOtherEligibleAccountsToConnect) { - Navigation.navigate(ROUTES.BANK_ACCOUNT_CONNECT_EXISTING_BUSINESS_BANK_ACCOUNT.getRoute(policyID, undefined, CONST.BANK_ACCOUNT.CONNECT_EXISTING_SOURCE.CHANGE_BANK_ACCOUNT)); - return; - } - - prepareNewBankAccountSetup(currency); - navigateToBankAccountRoute({policyID}); - }; + const handleChangeBankAccount = useChangeBankAccount(policyID, currency, reimbursementAccount?.achData?.bankAccountID); useResetBankAccountModal({ reimbursementAccount, diff --git a/src/pages/ReimbursementAccount/VerifiedBankAccountFlowEntryPoint.tsx b/src/pages/ReimbursementAccount/VerifiedBankAccountFlowEntryPoint.tsx index dc0e715e703d..fb846a870255 100644 --- a/src/pages/ReimbursementAccount/VerifiedBankAccountFlowEntryPoint.tsx +++ b/src/pages/ReimbursementAccount/VerifiedBankAccountFlowEntryPoint.tsx @@ -10,6 +10,7 @@ import Section from '@components/Section'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; +import useChangeBankAccount from '@hooks/useChangeBankAccount'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; @@ -22,20 +23,11 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {getLatestError, getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import {hasActiveAdminWorkspaces} from '@libs/PolicyUtils'; -import {getEligibleExistingBusinessBankAccounts} from '@libs/WorkflowUtils'; import {goToWithdrawalAccountSetupStep, openPlaidView, updateReimbursementAccountDraft} from '@userActions/BankAccounts'; import {setDraftValues} from '@userActions/FormActions'; import {openExternalLink} from '@userActions/Link'; -import { - navigateToBankAccountRoute, - prepareNewBankAccountSetup, - requestResetBankAccount, - resetReimbursementAccount, - setBankAccountSubStep, - setReimbursementAccountOptionPressed, - updateReimbursementAccount, -} from '@userActions/ReimbursementAccount'; +import {requestResetBankAccount, resetReimbursementAccount, setBankAccountSubStep, setReimbursementAccountOptionPressed, updateReimbursementAccount} from '@userActions/ReimbursementAccount'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -122,21 +114,7 @@ function VerifiedBankAccountFlowEntryPoint({ // The "Change bank account" option is only offered when opening a partially setup account from the Workflows > Payments section const isComingFromWorkflowsPayments = !!policyID && backTo === ROUTES.WORKSPACE_WORKFLOWS.getRoute(policyID); const shouldShowChangeBankAccount = shouldShowContinueSetupButton === true && isComingFromWorkflowsPayments; - const hasOtherEligibleAccountsToConnect = getEligibleExistingBusinessBankAccounts(bankAccountList, currency, true, reimbursementAccount?.achData?.bankAccountID).length > 0; - - const handleChangeBankAccount = () => { - if (!policyID || !currency) { - return; - } - - if (hasOtherEligibleAccountsToConnect) { - Navigation.navigate(ROUTES.BANK_ACCOUNT_CONNECT_EXISTING_BUSINESS_BANK_ACCOUNT.getRoute(policyID, undefined, CONST.BANK_ACCOUNT.CONNECT_EXISTING_SOURCE.CHANGE_BANK_ACCOUNT)); - return; - } - - prepareNewBankAccountSetup(currency); - navigateToBankAccountRoute({policyID}); - }; + const handleChangeBankAccount = useChangeBankAccount(policyID, currency, reimbursementAccount?.achData?.bankAccountID); const removeExistingBankAccountDetails = useCallback(() => { const bankAccountData: Partial = { diff --git a/tests/unit/hooks/useChangeBankAccount.test.ts b/tests/unit/hooks/useChangeBankAccount.test.ts new file mode 100644 index 000000000000..e176900daadd --- /dev/null +++ b/tests/unit/hooks/useChangeBankAccount.test.ts @@ -0,0 +1,116 @@ +import {act, renderHook} from '@testing-library/react-native'; + +import useChangeBankAccount from '@hooks/useChangeBankAccount'; + +import Navigation from '@navigation/Navigation'; + +import {navigateToBankAccountRoute, prepareNewBankAccountSetup} from '@userActions/ReimbursementAccount'; + +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type {BankAccountList} from '@src/types/onyx'; + +import Onyx from 'react-native-onyx'; + +import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; + +jest.mock('@navigation/Navigation', () => ({ + __esModule: true, + default: {navigate: jest.fn()}, +})); + +jest.mock('@userActions/ReimbursementAccount', () => ({ + __esModule: true, + prepareNewBankAccountSetup: jest.fn(), + navigateToBankAccountRoute: jest.fn(), +})); + +const POLICY_ID = '1'; +const CURRENCY = CONST.CURRENCY.USD; +const CURRENT_BANK_ACCOUNT_ID = 999; + +const mockNavigate = jest.mocked(Navigation.navigate); +const mockPrepareNewBankAccountSetup = jest.mocked(prepareNewBankAccountSetup); +const mockNavigateToBankAccountRoute = jest.mocked(navigateToBankAccountRoute); + +function buildBusinessBankAccount(methodID: number) { + return { + methodID, + bankCurrency: CURRENCY, + bankCountry: 'US', + accountData: { + state: CONST.BANK_ACCOUNT.STATE.OPEN, + type: CONST.BANK_ACCOUNT.TYPE.BUSINESS, + }, + }; +} + +// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion, @typescript-eslint/naming-convention +const OTHER_ELIGIBLE_BANK_ACCOUNT_LIST = {'111': buildBusinessBankAccount(111)} as unknown as BankAccountList; +// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion +const CURRENT_ONLY_BANK_ACCOUNT_LIST = {[`${CURRENT_BANK_ACCOUNT_ID}`]: buildBusinessBankAccount(CURRENT_BANK_ACCOUNT_ID)} as unknown as BankAccountList; + +async function setBankAccountList(bankAccountList: BankAccountList) { + await Onyx.merge(ONYXKEYS.BANK_ACCOUNT_LIST, bankAccountList); + await waitForBatchedUpdates(); +} + +describe('useChangeBankAccount', () => { + beforeAll(() => { + Onyx.init({keys: ONYXKEYS}); + }); + + beforeEach(async () => { + await Onyx.clear(); + await waitForBatchedUpdates(); + jest.clearAllMocks(); + }); + + afterEach(async () => { + await Onyx.clear(); + }); + + it('opens the existing-account picker when there are other eligible business bank accounts', async () => { + // The current account (999) plus another eligible open business account with a different methodID. + await setBankAccountList(OTHER_ELIGIBLE_BANK_ACCOUNT_LIST); + + const {result} = renderHook(() => useChangeBankAccount(POLICY_ID, CURRENCY, CURRENT_BANK_ACCOUNT_ID)); + await waitForBatchedUpdates(); + + act(() => result.current()); + + expect(mockNavigate).toHaveBeenCalledWith( + ROUTES.BANK_ACCOUNT_CONNECT_EXISTING_BUSINESS_BANK_ACCOUNT.getRoute(POLICY_ID, undefined, CONST.BANK_ACCOUNT.CONNECT_EXISTING_SOURCE.CHANGE_BANK_ACCOUNT), + ); + expect(mockPrepareNewBankAccountSetup).not.toHaveBeenCalled(); + expect(mockNavigateToBankAccountRoute).not.toHaveBeenCalled(); + }); + + it('starts a fresh setup when there are no other eligible business bank accounts', async () => { + // The only account is the current one, so it is excluded and nothing else is eligible. + await setBankAccountList(CURRENT_ONLY_BANK_ACCOUNT_LIST); + + const {result} = renderHook(() => useChangeBankAccount(POLICY_ID, CURRENCY, CURRENT_BANK_ACCOUNT_ID)); + await waitForBatchedUpdates(); + + act(() => result.current()); + + expect(mockPrepareNewBankAccountSetup).toHaveBeenCalledWith(CURRENCY); + expect(mockNavigateToBankAccountRoute).toHaveBeenCalledWith({policyID: POLICY_ID}); + expect(mockNavigate).not.toHaveBeenCalled(); + }); + + it('does nothing when policyID or currency is missing', async () => { + await setBankAccountList(OTHER_ELIGIBLE_BANK_ACCOUNT_LIST); + + const {result} = renderHook(() => useChangeBankAccount(undefined, undefined, CURRENT_BANK_ACCOUNT_ID)); + await waitForBatchedUpdates(); + + act(() => result.current()); + + expect(mockNavigate).not.toHaveBeenCalled(); + expect(mockPrepareNewBankAccountSetup).not.toHaveBeenCalled(); + expect(mockNavigateToBankAccountRoute).not.toHaveBeenCalled(); + }); +}); From 481ad632a221aed94cffaabb4b162e75a542f0fd Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 14 Jul 2026 11:26:06 +0200 Subject: [PATCH 35/46] Use Onyx selector for hasOtherEligibleAccountsToConnect --- src/hooks/useChangeBankAccount.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/hooks/useChangeBankAccount.ts b/src/hooks/useChangeBankAccount.ts index 2703e79a7efe..9f9755b504ef 100644 --- a/src/hooks/useChangeBankAccount.ts +++ b/src/hooks/useChangeBankAccount.ts @@ -16,14 +16,15 @@ import useOnyx from './useOnyx'; * Shared by ConnectedVerifiedBankAccount and VerifiedBankAccountFlowEntryPoint. */ function useChangeBankAccount(policyID: string | undefined, currency: string | undefined, bankAccountID: number | undefined) { - const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); + const [hasOtherEligibleAccountsToConnect = false] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST, { + selector: (bankAccountList) => getEligibleExistingBusinessBankAccounts(bankAccountList, currency, true, bankAccountID).length > 0, + }); return () => { if (!policyID || !currency) { return; } - const hasOtherEligibleAccountsToConnect = getEligibleExistingBusinessBankAccounts(bankAccountList, currency, true, bankAccountID).length > 0; if (hasOtherEligibleAccountsToConnect) { Navigation.navigate(ROUTES.BANK_ACCOUNT_CONNECT_EXISTING_BUSINESS_BANK_ACCOUNT.getRoute(policyID, undefined, CONST.BANK_ACCOUNT.CONNECT_EXISTING_SOURCE.CHANGE_BANK_ACCOUNT)); return; From 7cb322466ae97843fb3fb45218f250ff9dc1b790 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 14 Jul 2026 11:52:10 +0200 Subject: [PATCH 36/46] Move bank account eligibility check to a selector --- src/hooks/useChangeBankAccount.ts | 8 +-- src/selectors/ReimbursementAccount.ts | 14 +++- .../selectors/ReimbursementAccountTest.ts | 70 +++++++++++++++++++ 3 files changed, 85 insertions(+), 7 deletions(-) create mode 100644 tests/unit/selectors/ReimbursementAccountTest.ts diff --git a/src/hooks/useChangeBankAccount.ts b/src/hooks/useChangeBankAccount.ts index 9f9755b504ef..96667271a4a9 100644 --- a/src/hooks/useChangeBankAccount.ts +++ b/src/hooks/useChangeBankAccount.ts @@ -1,5 +1,3 @@ -import {getEligibleExistingBusinessBankAccounts} from '@libs/WorkflowUtils'; - import Navigation from '@navigation/Navigation'; import {navigateToBankAccountRoute, prepareNewBankAccountSetup} from '@userActions/ReimbursementAccount'; @@ -8,6 +6,8 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import {hasOtherEligibleBusinessBankAccountsSelector} from '@selectors/ReimbursementAccount'; + import useOnyx from './useOnyx'; /** @@ -16,9 +16,7 @@ import useOnyx from './useOnyx'; * Shared by ConnectedVerifiedBankAccount and VerifiedBankAccountFlowEntryPoint. */ function useChangeBankAccount(policyID: string | undefined, currency: string | undefined, bankAccountID: number | undefined) { - const [hasOtherEligibleAccountsToConnect = false] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST, { - selector: (bankAccountList) => getEligibleExistingBusinessBankAccounts(bankAccountList, currency, true, bankAccountID).length > 0, - }); + const [hasOtherEligibleAccountsToConnect = false] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST, {selector: hasOtherEligibleBusinessBankAccountsSelector(currency, bankAccountID)}); return () => { if (!policyID || !currency) { diff --git a/src/selectors/ReimbursementAccount.ts b/src/selectors/ReimbursementAccount.ts index c1e7508a820b..62180756f2fa 100644 --- a/src/selectors/ReimbursementAccount.ts +++ b/src/selectors/ReimbursementAccount.ts @@ -1,4 +1,6 @@ -import type {ReimbursementAccount} from '@src/types/onyx'; +import {getEligibleExistingBusinessBankAccounts} from '@libs/WorkflowUtils'; + +import type {BankAccountList, ReimbursementAccount} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type {OnyxEntry} from 'react-native-onyx'; @@ -7,4 +9,12 @@ const reimbursementAccountErrorSelector = (reimbursementAccount: OnyxEntry) => !isEmptyObject(reimbursementAccount?.errors); -export {reimbursementAccountErrorSelector, hasReimbursementAccountErrorsSelector}; +/** + * Returns a selector that reports whether there are other eligible existing business bank accounts that could be connected. + */ +const hasOtherEligibleBusinessBankAccountsSelector = + (currency: string | undefined, excludeBankAccountID: number | undefined) => + (bankAccountList: OnyxEntry): boolean => + getEligibleExistingBusinessBankAccounts(bankAccountList, currency, true, excludeBankAccountID).length > 0; + +export {reimbursementAccountErrorSelector, hasReimbursementAccountErrorsSelector, hasOtherEligibleBusinessBankAccountsSelector}; diff --git a/tests/unit/selectors/ReimbursementAccountTest.ts b/tests/unit/selectors/ReimbursementAccountTest.ts new file mode 100644 index 000000000000..f681d6dda5e4 --- /dev/null +++ b/tests/unit/selectors/ReimbursementAccountTest.ts @@ -0,0 +1,70 @@ +import CONST from '@src/CONST'; +import type {BankAccount, BankAccountList} from '@src/types/onyx'; + +import {hasOtherEligibleBusinessBankAccountsSelector} from '@selectors/ReimbursementAccount'; + +const CURRENCY = 'USD'; + +function createBankAccount(methodID: number, overrides?: {currency?: string; state?: string; type?: string}): BankAccount { + return { + methodID, + bankCurrency: overrides?.currency ?? CURRENCY, + bankCountry: CONST.COUNTRY.US, + accountData: { + state: overrides?.state ?? CONST.BANK_ACCOUNT.STATE.OPEN, + type: overrides?.type ?? CONST.BANK_ACCOUNT.TYPE.BUSINESS, + }, + }; +} + +describe('hasOtherEligibleBusinessBankAccountsSelector', () => { + it('returns false when the bank account list is undefined or empty', () => { + expect(hasOtherEligibleBusinessBankAccountsSelector(CURRENCY, undefined)(undefined)).toBe(false); + expect(hasOtherEligibleBusinessBankAccountsSelector(CURRENCY, undefined)({})).toBe(false); + }); + + it('returns false when the currency is undefined', () => { + const bankAccountList: BankAccountList = {a1: createBankAccount(1)}; + expect(hasOtherEligibleBusinessBankAccountsSelector(undefined, undefined)(bankAccountList)).toBe(false); + }); + + it('returns true when there is an open business bank account in the matching currency', () => { + const bankAccountList: BankAccountList = {a1: createBankAccount(1)}; + expect(hasOtherEligibleBusinessBankAccountsSelector(CURRENCY, undefined)(bankAccountList)).toBe(true); + }); + + it('returns true for partially set up business bank accounts (SETUP, VERIFYING, PENDING)', () => { + for (const state of [CONST.BANK_ACCOUNT.STATE.SETUP, CONST.BANK_ACCOUNT.STATE.VERIFYING, CONST.BANK_ACCOUNT.STATE.PENDING]) { + const bankAccountList: BankAccountList = {a1: createBankAccount(1, {state})}; + expect(hasOtherEligibleBusinessBankAccountsSelector(CURRENCY, undefined)(bankAccountList)).toBe(true); + } + }); + + it('ignores accounts in a non-eligible state (e.g. DELETED)', () => { + const bankAccountList: BankAccountList = {a1: createBankAccount(1, {state: CONST.BANK_ACCOUNT.STATE.DELETED})}; + expect(hasOtherEligibleBusinessBankAccountsSelector(CURRENCY, undefined)(bankAccountList)).toBe(false); + }); + + it('ignores accounts with a mismatched currency', () => { + const bankAccountList: BankAccountList = {a1: createBankAccount(1, {currency: 'EUR'})}; + expect(hasOtherEligibleBusinessBankAccountsSelector(CURRENCY, undefined)(bankAccountList)).toBe(false); + }); + + it('ignores personal (non-business) bank accounts', () => { + const bankAccountList: BankAccountList = {a1: createBankAccount(1, {type: CONST.BANK_ACCOUNT.TYPE.PERSONAL})}; + expect(hasOtherEligibleBusinessBankAccountsSelector(CURRENCY, undefined)(bankAccountList)).toBe(false); + }); + + it('excludes the current bank account by methodID', () => { + const bankAccountList: BankAccountList = {a1: createBankAccount(1)}; + expect(hasOtherEligibleBusinessBankAccountsSelector(CURRENCY, 1)(bankAccountList)).toBe(false); + }); + + it('returns true when another eligible account exists besides the excluded one', () => { + const bankAccountList: BankAccountList = { + a1: createBankAccount(1), + a2: createBankAccount(2), + }; + expect(hasOtherEligibleBusinessBankAccountsSelector(CURRENCY, 1)(bankAccountList)).toBe(true); + }); +}); From 44021547b8c987003bfd0e21a45e180a76af59c4 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Tue, 14 Jul 2026 12:09:14 +0200 Subject: [PATCH 37/46] Put ONYXKEYS.IS_CHANGING_TO_NEW_BANK_ACCOUNT into bucket --- src/libs/ExportOnyxState/common.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/ExportOnyxState/common.ts b/src/libs/ExportOnyxState/common.ts index cf4ad366f553..fcd71692092b 100644 --- a/src/libs/ExportOnyxState/common.ts +++ b/src/libs/ExportOnyxState/common.ts @@ -178,6 +178,7 @@ const safeOnyxKeys = new Set([ ONYXKEYS.IMPORTED_SPREADSHEET_MEMBER_ROLE, ONYXKEYS.INPUT_FOCUSED, ONYXKEYS.IS_BETA, + ONYXKEYS.IS_CHANGING_TO_NEW_BANK_ACCOUNT, ONYXKEYS.IS_COMING_FROM_GLOBAL_REIMBURSEMENTS_FLOW, ONYXKEYS.IS_DEBUG_MODE_ENABLED, ONYXKEYS.IS_GPS_IN_PROGRESS_MODAL_OPEN, From 51eb681f0b67d1cadb5b05209d9bf8d03a464a0a Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 15 Jul 2026 12:46:18 +0200 Subject: [PATCH 38/46] Fix flicker and back navigation when changing bank account --- src/ROUTES.ts | 5 ++- src/hooks/useChangeBankAccount.ts | 5 ++- src/libs/Navigation/types.ts | 1 + .../ReimbursementAccount/navigation.ts | 5 ++- .../ReimbursementAccountPage.tsx | 36 ++++++++++++++++--- .../VerifiedBankAccountFlowEntryPoint.tsx | 20 +++++++++-- 6 files changed, 61 insertions(+), 11 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index cc456f5cea19..e5791d60e8c7 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1271,13 +1271,16 @@ const ROUTES = { // TODO: rename the route as no longer accepts step BANK_ACCOUNT_WITH_STEP_TO_OPEN: { route: 'bank-account/new', - getRoute: ({policyID, bankAccountID, backTo}: {policyID: string | undefined; bankAccountID?: number; backTo?: string}) => { + getRoute: ({policyID, bankAccountID, backTo, isChangingBankAccount}: {policyID: string | undefined; bankAccountID?: number; backTo?: string; isChangingBankAccount?: boolean}) => { let queryString = ''; if (bankAccountID) { queryString = `?bankAccountID=${bankAccountID}`; } else if (policyID) { queryString = `?policyID=${policyID}`; } + if (isChangingBankAccount) { + queryString += queryString ? '&isChangingBankAccount=true' : '?isChangingBankAccount=true'; + } // TODO this backTo comes from drilling it through bank account form screens // should be removed once https://github.com/Expensify/App/pull/72219 is resolved return getUrlWithBackToParam(`bank-account/new${queryString}`, backTo); diff --git a/src/hooks/useChangeBankAccount.ts b/src/hooks/useChangeBankAccount.ts index 96667271a4a9..efb8ca8feac0 100644 --- a/src/hooks/useChangeBankAccount.ts +++ b/src/hooks/useChangeBankAccount.ts @@ -1,6 +1,6 @@ import Navigation from '@navigation/Navigation'; -import {navigateToBankAccountRoute, prepareNewBankAccountSetup} from '@userActions/ReimbursementAccount'; +import {navigateToBankAccountRoute} from '@userActions/ReimbursementAccount'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -28,8 +28,7 @@ function useChangeBankAccount(policyID: string | undefined, currency: string | u return; } - prepareNewBankAccountSetup(currency); - navigateToBankAccountRoute({policyID}); + navigateToBankAccountRoute({policyID, isChangingBankAccount: true}); }; } diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 8575a5134d70..f3fd334b0988 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -2433,6 +2433,7 @@ type ReimbursementAccountNavigatorParamList = { policyID?: string; bankAccountID?: string; subStep?: typeof CONST.BANK_ACCOUNT.STEP.COUNTRY; + isChangingBankAccount?: boolean; }; [SCREENS.REIMBURSEMENT_ACCOUNT_USD]: { page?: string; diff --git a/src/libs/actions/ReimbursementAccount/navigation.ts b/src/libs/actions/ReimbursementAccount/navigation.ts index d0fc98280170..2110015d5212 100644 --- a/src/libs/actions/ReimbursementAccount/navigation.ts +++ b/src/libs/actions/ReimbursementAccount/navigation.ts @@ -24,6 +24,7 @@ function goToWithdrawalAccountSetupStep(stepID: BankAccountStep) { * @param [navigationOptions] - Optional navigation options to customize the navigation behavior. * @param [policyCurrency] - The policy output currency. Used to detect a USD account that should skip straight to validation. * @param [bankAccountState] - The current bank account state. Used to detect a pending account that should skip straight to validation. + * @param [isChangingBankAccount] - Whether the user is in change bank account flow. */ function navigateToBankAccountRoute({ policyID = '', @@ -32,6 +33,7 @@ function navigateToBankAccountRoute({ navigationOptions, policyCurrency, bankAccountState, + isChangingBankAccount, }: { policyID?: string; bankAccountID?: number; @@ -39,6 +41,7 @@ function navigateToBankAccountRoute({ navigationOptions?: LinkToOptions; policyCurrency?: string; bankAccountState?: string; + isChangingBankAccount?: boolean; }) { // If USD bank account is in pending state, we should navigate straight to the validation step and skip the Continue setup step if (policyCurrency === CONST.CURRENCY.USD && bankAccountState === CONST.BANK_ACCOUNT.STATE.PENDING) { @@ -46,7 +49,7 @@ function navigateToBankAccountRoute({ return; } - Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute({policyID, bankAccountID, backTo}), navigationOptions); + Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute({policyID, bankAccountID, backTo, isChangingBankAccount}), navigationOptions); } export {goToWithdrawalAccountSetupStep, navigateToBankAccountRoute}; diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx index 2216f542b0bf..b67212fc4ee6 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx @@ -104,6 +104,7 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy}: Reimbursemen const bankAccountIDParam = route.params?.bankAccountID; const subStepParam = route.params?.subStep; const backTo = route.params?.backTo; + const isChangingBankAccount = !!route.params?.isChangingBankAccount; const isComingFromExpensifyCard = (backTo as string)?.includes(CONST.EXPENSIFY_CARD.ROUTE as string); const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -111,6 +112,8 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy}: Reimbursemen const requestorStepRef = useRef(null); const hasRequestedNewBankAccountRef = useRef(false); const hasClearedStalePlaidErrorsRef = useRef(false); + const isChangingBankAccountRef = useRef(isChangingBankAccount); + const hasShownConnectedBankAccountRef = useRef(false); const prevReimbursementAccount = usePrevious(reimbursementAccount); const prevIsOffline = usePrevious(isOffline); const achData = reimbursementAccount?.achData; @@ -171,9 +174,12 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy}: Reimbursemen const [isNonUSDSetup, setIsNonUSDSetup] = useState(policy ? isNonUSDWorkspace : achData?.currency !== CONST.CURRENCY.USD || reimbursementAccountDraft?.currency !== CONST.CURRENCY.USD); useEffect(() => { + const isChangingBankAccountInstance = isChangingBankAccountRef.current; return () => { - clearReimbursementAccountDraft(); - clearReimbursementAccount(); + if (!isChangingBankAccountInstance) { + clearReimbursementAccountDraft(); + clearReimbursementAccount(); + } cancelChangingToNewBankAccount(); getPaymentMethods(); }; @@ -270,6 +276,22 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy}: Reimbursemen // eslint-disable-next-line react-hooks/exhaustive-deps }, [isLoadingWorkspaceReimbursement, prevIsLoadingWorkspaceReimbursement]); + // A pushed "change bank account" instance and its setup steps mutate the shared reimbursement account. When focus + // returns to this connected (non-changing) screen and the shared data has been clobbered, reload it so the + // "you're all set" page is shown again instead of the setup entry. + useEffect(() => { + const isConnectedBankAccount = isNonUSDSetup ? achData?.state === CONST.BANK_ACCOUNT.STATE.OPEN : achData?.currentStep === CONST.BANK_ACCOUNT.STEP.ENABLE; + if (isConnectedBankAccount && !isChangingBankAccount) { + hasShownConnectedBankAccountRef.current = true; + return; + } + if (isFocused && !isChangingBankAccount && hasShownConnectedBankAccountRef.current && !isConnectedBankAccount) { + fetchData(); + } + // fetchData is intentionally omitted; this must react to the connected data being clobbered, not to fetchData's identity. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isFocused, isChangingBankAccount, isNonUSDSetup, achData?.state, achData?.currentStep]); + useEffect(() => { // Consume this route intent only once so the response changing isPreviousPolicy does not trigger another request. const shouldOpenNewBankAccount = route.params?.stepToOpen === REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.NEW && !hasRequestedNewBankAccountRef.current; @@ -570,7 +592,9 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy}: Reimbursemen const isConnectedVerifiedBankAccountData = isNonUSDSetup ? achData?.state === CONST.BANK_ACCOUNT.STATE.OPEN : achData?.currentStep === CONST.BANK_ACCOUNT.STEP.ENABLE; - if (shouldShowConnectedVerifiedBankAccount && isConnectedVerifiedBankAccountData) { + // While this instance is starting a fresh setup (change bank account), show the setup entry instead of the connected + // account, even though the shared data still describes the currently connected account. + if (shouldShowConnectedVerifiedBankAccount && isConnectedVerifiedBankAccountData && !isChangingBankAccount) { if (topmostFullScreenRoute?.name === NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR) { return ( @@ -601,7 +625,10 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy}: Reimbursemen } // Once fresh data has loaded, trust the live value to avoid a one-frame flash from the effect-synced state lagging achData. - const shouldShowContinueSetupButtonToDisplay = hasLoadedData ? shouldShowContinueSetupButtonValue : shouldShowContinueSetupButton; + const shouldShowContinueSetupButtonWhenLoaded = hasLoadedData ? shouldShowContinueSetupButtonValue : shouldShowContinueSetupButton; + // On a "change bank account" instance always show the fresh entry (never "continue setup"), since the shared data still + // describes the previous account/in-progress setup that this flow is replacing. + const shouldShowContinueSetupButtonToDisplay = isChangingBankAccount ? false : shouldShowContinueSetupButtonWhenLoaded; return ( ); } diff --git a/src/pages/ReimbursementAccount/VerifiedBankAccountFlowEntryPoint.tsx b/src/pages/ReimbursementAccount/VerifiedBankAccountFlowEntryPoint.tsx index fb846a870255..bdad8479ab59 100644 --- a/src/pages/ReimbursementAccount/VerifiedBankAccountFlowEntryPoint.tsx +++ b/src/pages/ReimbursementAccount/VerifiedBankAccountFlowEntryPoint.tsx @@ -27,7 +27,14 @@ import {hasActiveAdminWorkspaces} from '@libs/PolicyUtils'; import {goToWithdrawalAccountSetupStep, openPlaidView, updateReimbursementAccountDraft} from '@userActions/BankAccounts'; import {setDraftValues} from '@userActions/FormActions'; import {openExternalLink} from '@userActions/Link'; -import {requestResetBankAccount, resetReimbursementAccount, setBankAccountSubStep, setReimbursementAccountOptionPressed, updateReimbursementAccount} from '@userActions/ReimbursementAccount'; +import { + prepareNewBankAccountSetup, + requestResetBankAccount, + resetReimbursementAccount, + setBankAccountSubStep, + setReimbursementAccountOptionPressed, + updateReimbursementAccount, +} from '@userActions/ReimbursementAccount'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -74,6 +81,9 @@ type VerifiedBankAccountFlowEntryPointProps = { /** Whether the user is coming from the expensify card */ isComingFromExpensifyCard?: boolean; + + /** Whether this instance is starting a fresh setup from a "change bank account" flow */ + isChangingBankAccount?: boolean; }; const bankInfoStepKeys = INPUT_IDS.BANK_INFO_STEP; @@ -89,6 +99,7 @@ function VerifiedBankAccountFlowEntryPoint({ setUSDBankAccountStep, setShouldShowContinueSetupButton, isComingFromExpensifyCard, + isChangingBankAccount, }: VerifiedBankAccountFlowEntryPointProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -117,6 +128,11 @@ function VerifiedBankAccountFlowEntryPoint({ const handleChangeBankAccount = useChangeBankAccount(policyID, currency, reimbursementAccount?.achData?.bankAccountID); const removeExistingBankAccountDetails = useCallback(() => { + // In a "change bank account" flow, start a completely fresh setup so the new account's steps aren't prefilled. + if (isChangingBankAccount && currency) { + prepareNewBankAccountSetup(currency); + return; + } const bankAccountData: Partial = { [bankInfoStepKeys.ROUTING_NUMBER]: '', [bankInfoStepKeys.ACCOUNT_NUMBER]: '', @@ -128,7 +144,7 @@ function VerifiedBankAccountFlowEntryPoint({ }; updateReimbursementAccountDraft(bankAccountData); updateReimbursementAccount({bankAccountID: 0}); - }, []); + }, [isChangingBankAccount, currency]); /** * Prepares and redirects user to next step in the USD flow From f781dbf864265e984b099155cc3c03f9850f48f8 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 15 Jul 2026 13:00:48 +0200 Subject: [PATCH 39/46] Minor clean up --- .../ReimbursementAccount/ReimbursementAccountPage.tsx | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx index b67212fc4ee6..cfba0a3fea64 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx @@ -172,6 +172,7 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy}: Reimbursemen const currentStep = getInitialCurrentStep(); const [USDBankAccountStep, setUSDBankAccountStep] = useState(subStepParam ?? null); const [isNonUSDSetup, setIsNonUSDSetup] = useState(policy ? isNonUSDWorkspace : achData?.currency !== CONST.CURRENCY.USD || reimbursementAccountDraft?.currency !== CONST.CURRENCY.USD); + const isConnectedVerifiedBankAccountData = isNonUSDSetup ? achData?.state === CONST.BANK_ACCOUNT.STATE.OPEN : achData?.currentStep === CONST.BANK_ACCOUNT.STEP.ENABLE; useEffect(() => { const isChangingBankAccountInstance = isChangingBankAccountRef.current; @@ -280,17 +281,16 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy}: Reimbursemen // returns to this connected (non-changing) screen and the shared data has been clobbered, reload it so the // "you're all set" page is shown again instead of the setup entry. useEffect(() => { - const isConnectedBankAccount = isNonUSDSetup ? achData?.state === CONST.BANK_ACCOUNT.STATE.OPEN : achData?.currentStep === CONST.BANK_ACCOUNT.STEP.ENABLE; - if (isConnectedBankAccount && !isChangingBankAccount) { + if (isConnectedVerifiedBankAccountData && !isChangingBankAccount) { hasShownConnectedBankAccountRef.current = true; return; } - if (isFocused && !isChangingBankAccount && hasShownConnectedBankAccountRef.current && !isConnectedBankAccount) { + if (isFocused && !isChangingBankAccount && hasShownConnectedBankAccountRef.current && !isConnectedVerifiedBankAccountData) { fetchData(); } // fetchData is intentionally omitted; this must react to the connected data being clobbered, not to fetchData's identity. // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isFocused, isChangingBankAccount, isNonUSDSetup, achData?.state, achData?.currentStep]); + }, [isFocused, isChangingBankAccount, isConnectedVerifiedBankAccountData]); useEffect(() => { // Consume this route intent only once so the response changing isPreviousPolicy does not trigger another request. @@ -590,8 +590,6 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy}: Reimbursemen ); } - const isConnectedVerifiedBankAccountData = isNonUSDSetup ? achData?.state === CONST.BANK_ACCOUNT.STATE.OPEN : achData?.currentStep === CONST.BANK_ACCOUNT.STEP.ENABLE; - // While this instance is starting a fresh setup (change bank account), show the setup entry instead of the connected // account, even though the shared data still describes the currently connected account. if (shouldShowConnectedVerifiedBankAccount && isConnectedVerifiedBankAccountData && !isChangingBankAccount) { From 426016f81df72abcea498d0a3c15eb9c78173165 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 15 Jul 2026 13:07:23 +0200 Subject: [PATCH 40/46] Simplify const --- src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx index cfba0a3fea64..72cc65c9f167 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx @@ -623,10 +623,8 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy}: Reimbursemen } // Once fresh data has loaded, trust the live value to avoid a one-frame flash from the effect-synced state lagging achData. - const shouldShowContinueSetupButtonWhenLoaded = hasLoadedData ? shouldShowContinueSetupButtonValue : shouldShowContinueSetupButton; - // On a "change bank account" instance always show the fresh entry (never "continue setup"), since the shared data still - // describes the previous account/in-progress setup that this flow is replacing. - const shouldShowContinueSetupButtonToDisplay = isChangingBankAccount ? false : shouldShowContinueSetupButtonWhenLoaded; + // On a "change bank account" instance never show "continue setup", since the shared data still describes the account being replaced. + const shouldShowContinueSetupButtonToDisplay = !isChangingBankAccount && (hasLoadedData ? shouldShowContinueSetupButtonValue : shouldShowContinueSetupButton); return ( Date: Wed, 15 Jul 2026 13:11:16 +0200 Subject: [PATCH 41/46] Fix test --- tests/unit/hooks/useChangeBankAccount.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/unit/hooks/useChangeBankAccount.test.ts b/tests/unit/hooks/useChangeBankAccount.test.ts index e176900daadd..4033d0f4b797 100644 --- a/tests/unit/hooks/useChangeBankAccount.test.ts +++ b/tests/unit/hooks/useChangeBankAccount.test.ts @@ -96,8 +96,7 @@ describe('useChangeBankAccount', () => { act(() => result.current()); - expect(mockPrepareNewBankAccountSetup).toHaveBeenCalledWith(CURRENCY); - expect(mockNavigateToBankAccountRoute).toHaveBeenCalledWith({policyID: POLICY_ID}); + expect(mockNavigateToBankAccountRoute).toHaveBeenCalledWith({policyID: POLICY_ID, isChangingBankAccount: true}); expect(mockNavigate).not.toHaveBeenCalled(); }); From 6c6c49dc0ed36e732ab1701fe628b0db42b2a678 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 15 Jul 2026 16:31:57 +0200 Subject: [PATCH 42/46] Correct the flag name --- src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx index 72cc65c9f167..a33d4e31b886 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.tsx @@ -96,7 +96,7 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy}: Reimbursemen const [onfidoToken = ''] = useOnyx(ONYXKEYS.ONFIDO_TOKEN); const [isLoadingApp = false] = useOnyx(ONYXKEYS.IS_LOADING_APP); const topmostFullScreenRoute = useRootNavigationState((state) => state?.routes.findLast((lastRoute) => isFullScreenName(lastRoute.name))); - const [isChangingBusinessBankAccount] = useOnyx(ONYXKEYS.IS_CHANGING_TO_NEW_BANK_ACCOUNT); + const [isChangingToNewBankAccount] = useOnyx(ONYXKEYS.IS_CHANGING_TO_NEW_BANK_ACCOUNT); const {isBetaEnabled} = usePermissions(); const policyName = policy?.name ?? ''; @@ -301,7 +301,7 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy}: Reimbursemen // Skip while switching the workspace's bank account: the dedicated effect above fetches once the switch // finishes, so fetching here would race it and could load the old account. - if (isChangingBusinessBankAccount || isLoadingWorkspaceReimbursement) { + if (isChangingToNewBankAccount || isLoadingWorkspaceReimbursement) { return; } @@ -542,7 +542,7 @@ function ReimbursementAccountPage({route, policy, isLoadingPolicy}: Reimbursemen // On Android, when we open the app from the background, Onfido activity gets destroyed, so we need to reopen it. if ( (!!policyIDParam || !!bankAccountIDParam) && - !isChangingBusinessBankAccount && + !isChangingToNewBankAccount && (!hasACHDataBeenLoaded || isLoading || isLoadingWorkspaceReimbursement || isSettingBA) && shouldShowOfflineLoader && (shouldReopenOnfido || !requestorStepRef?.current) From 9f3f62ee9a441ec77c19ca62882d217ad0a8e733 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 15 Jul 2026 17:06:28 +0200 Subject: [PATCH 43/46] Reset bank account details before non-USD change flow setup --- .../VerifiedBankAccountFlowEntryPoint.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/ReimbursementAccount/VerifiedBankAccountFlowEntryPoint.tsx b/src/pages/ReimbursementAccount/VerifiedBankAccountFlowEntryPoint.tsx index bdad8479ab59..fc8f05ed7138 100644 --- a/src/pages/ReimbursementAccount/VerifiedBankAccountFlowEntryPoint.tsx +++ b/src/pages/ReimbursementAccount/VerifiedBankAccountFlowEntryPoint.tsx @@ -169,6 +169,7 @@ function VerifiedBankAccountFlowEntryPoint({ } if (reimbursementAccountOptionPressed === CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL) { + removeExistingBankAccountDetails(); if (isNonUSDWorkspace) { if (isComingFromExpensifyCard) { setDraftValues(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM, {isComingFromExpensifyCard}); @@ -178,7 +179,6 @@ function VerifiedBankAccountFlowEntryPoint({ return; } - removeExistingBankAccountDetails(); prepareNextStep(CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL); setReimbursementAccountOptionPressed(CONST.BANK_ACCOUNT.SETUP_TYPE.NONE); } else if (reimbursementAccountOptionPressed === CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID) { @@ -196,6 +196,8 @@ function VerifiedBankAccountFlowEntryPoint({ return; } + removeExistingBankAccountDetails(); + if (isNonUSDWorkspace) { if (isComingFromExpensifyCard) { setDraftValues(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM, {isComingFromExpensifyCard}); @@ -204,7 +206,6 @@ function VerifiedBankAccountFlowEntryPoint({ return; } - removeExistingBankAccountDetails(); prepareNextStep(CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL); }; From 491e02a6e5f20605886a013169c76b9e605c747b Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 15 Jul 2026 17:17:13 +0200 Subject: [PATCH 44/46] Pass isChangingBankAccount param --- src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx b/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx index b0b29fdfcd06..9abdff58dc81 100644 --- a/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx +++ b/src/pages/workspace/ConnectExistingBusinessBankAccountPage.tsx @@ -66,7 +66,7 @@ function ConnectExistingBusinessBankAccountPage({route}: ConnectExistingBusiness } Navigation.setNavigationActionToMicrotaskQueue(() => { - Navigation.navigate(appendParam(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute({policyID, backTo}), 'stepToOpen', REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.NEW)); + Navigation.navigate(appendParam(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute({policyID, backTo, isChangingBankAccount}), 'stepToOpen', REIMBURSEMENT_ACCOUNT_ROUTE_NAMES.NEW)); }); }; From 03b2309cda1dfe0b01ca200f10e4a338fd4f13c4 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 15 Jul 2026 17:52:20 +0200 Subject: [PATCH 45/46] Re-run checks From 30592f9f3d0c44b9df1e3f2c6aa44034e628f1f6 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Thu, 16 Jul 2026 11:05:04 +0200 Subject: [PATCH 46/46] Fix non-payer admin bank account change flow on Workflows --- .../workspace/workflows/WorkspaceWorkflowsPage.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx index 3937413adf20..c21c7375ae11 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx @@ -401,6 +401,7 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) { const isAccountInSetupState = isBankAccountPartiallySetup(state); const isBusinessBankAccountLocked = state === CONST.BANK_ACCOUNT.STATE.LOCKED; const canChangePayer = canWritePayments && !isAccountInSetupState; + const hasOtherEligibleExistingAccounts = getEligibleExistingBusinessBankAccounts(bankAccountList, policy?.outputCurrency, true, bankAccountID).length > 0; const shouldShowBankAccount = (!!isBankAccountFullySetup || !!bankAccountConnectedToWorkspace) && policy?.reimbursementChoice !== CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO; const shouldShowPayer = shouldShowBankAccount || policy?.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL; @@ -702,13 +703,11 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) { return; } - // User who is not reimburser can't initiate unlocking process but can connect new account - if (state === CONST.BANK_ACCOUNT.STATE.LOCKED && bankAccountID && !isUserReimburser) { - // If user has existing accounts and no bank account setup in progress we should show screen to choose an existing account - if (hasValidExistingAccounts && !shouldShowContinueModal) { - Navigation.navigate(ROUTES.BANK_ACCOUNT_CONNECT_EXISTING_BUSINESS_BANK_ACCOUNT.getRoute(route.params.policyID)); - return; - } + // A non-reimburser can't edit or unlock the workspace's connected account, so if they have another + // eligible existing account and no setup in progress, let them link it (change the workspace's account). + if (!isUserReimburser && hasOtherEligibleExistingAccounts && !shouldShowContinueModal) { + Navigation.navigate(ROUTES.BANK_ACCOUNT_CONNECT_EXISTING_BUSINESS_BANK_ACCOUNT.getRoute(route.params.policyID)); + return; } navigateToBankAccountRoute({