From e76920048a1cd6f2f6248395de1bd2ef398ce1da Mon Sep 17 00:00:00 2001 From: "Eugene Voloshchak (via MelvinBot)" Date: Sun, 10 May 2026 21:41:47 +0000 Subject: [PATCH 1/4] Add BANK_ACCOUNT_LIST cleanup to resetUSDBankAccount and resetNonUSDBankAccount When resetting a bank account via "Start over", the BANK_ACCOUNT_LIST entry was not being cleared. This caused the pending bank account row to persist in WorkspaceWorkflowsPage after app restart while offline, since that page checks BANK_ACCOUNT_LIST for entries matching the workspace policyID. Following the existing pattern in deletePaymentBankAccount, this adds BANK_ACCOUNT_LIST updates to optimistic/success/failure data in both resetUSDBankAccount and resetNonUSDBankAccount. Co-authored-by: Eugene Voloshchak --- .../resetNonUSDBankAccount.ts | 21 ++++++++++++++++++- .../resetUSDBankAccount.ts | 16 ++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/ReimbursementAccount/resetNonUSDBankAccount.ts b/src/libs/actions/ReimbursementAccount/resetNonUSDBankAccount.ts index f4145d0631e0..2921f6229155 100644 --- a/src/libs/actions/ReimbursementAccount/resetNonUSDBankAccount.ts +++ b/src/libs/actions/ReimbursementAccount/resetNonUSDBankAccount.ts @@ -38,7 +38,11 @@ function resetNonUSDBankAccount(policyID: string | undefined, achAccount: OnyxEn const isPreviousLastUsedPaymentMethodVBBA = lastUsedPaymentMethod?.lastUsed?.name === CONST.IOU.PAYMENT_TYPE.VBBA; const onyxData: OnyxData< - typeof ONYXKEYS.NVP_LAST_PAYMENT_METHOD | typeof ONYXKEYS.REIMBURSEMENT_ACCOUNT | typeof ONYXKEYS.COLLECTION.POLICY | typeof ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT + | typeof ONYXKEYS.NVP_LAST_PAYMENT_METHOD + | typeof ONYXKEYS.REIMBURSEMENT_ACCOUNT + | typeof ONYXKEYS.COLLECTION.POLICY + | typeof ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT + | typeof ONYXKEYS.BANK_ACCOUNT_LIST > = { optimisticData: [ { @@ -51,6 +55,11 @@ function resetNonUSDBankAccount(policyID: string | undefined, achAccount: OnyxEn achData: null, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.BANK_ACCOUNT_LIST, + value: {[bankAccountID]: {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}}, + }, ], successData: [ { @@ -63,6 +72,11 @@ function resetNonUSDBankAccount(policyID: string | undefined, achAccount: OnyxEn key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, value: CONST.REIMBURSEMENT_ACCOUNT.DEFAULT_DATA, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.BANK_ACCOUNT_LIST, + value: {[bankAccountID]: null}, + }, ], failureData: [ { @@ -70,6 +84,11 @@ function resetNonUSDBankAccount(policyID: string | undefined, achAccount: OnyxEn key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, value: {isLoading: false, pendingAction: null}, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.BANK_ACCOUNT_LIST, + value: {[bankAccountID]: {pendingAction: null}}, + }, ], }; diff --git a/src/libs/actions/ReimbursementAccount/resetUSDBankAccount.ts b/src/libs/actions/ReimbursementAccount/resetUSDBankAccount.ts index ce33a5b144cc..15d5e13f43f4 100644 --- a/src/libs/actions/ReimbursementAccount/resetUSDBankAccount.ts +++ b/src/libs/actions/ReimbursementAccount/resetUSDBankAccount.ts @@ -38,6 +38,7 @@ function resetUSDBankAccount( | typeof ONYXKEYS.RAM_ONLY_PLAID_LINK_TOKEN | typeof ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT | typeof ONYXKEYS.NVP_LAST_PAYMENT_METHOD + | typeof ONYXKEYS.BANK_ACCOUNT_LIST > = { optimisticData: [ { @@ -57,6 +58,11 @@ function resetUSDBankAccount( achAccount: null, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.BANK_ACCOUNT_LIST, + value: {[bankAccountID]: {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}}, + }, ], successData: [ { @@ -128,6 +134,11 @@ function resetUSDBankAccount( [INPUT_IDS.AMOUNT3]: '', }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.BANK_ACCOUNT_LIST, + value: {[bankAccountID]: null}, + }, ], failureData: [ { @@ -142,6 +153,11 @@ function resetUSDBankAccount( achAccount, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.BANK_ACCOUNT_LIST, + value: {[bankAccountID]: {pendingAction: null}}, + }, ], }; From 5a005dc97ea527d8bd8f6767e570e275ca86f757 Mon Sep 17 00:00:00 2001 From: "Eugene Voloshchak (via MelvinBot)" Date: Mon, 25 May 2026 08:54:03 +0000 Subject: [PATCH 2/4] Show bank account as deleted (strikethrough) when pending deletion Wrap bank account MenuItem with OfflineWithFeedback passing the bankAccountPendingAction so the row appears struck through while the delete is in flight offline. Co-authored-by: Eugene Voloshchak --- src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx index 4e13e60f3a2d..43dde60fe6d2 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx @@ -322,6 +322,7 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) { const isBusinessBankAccountLocked = state === CONST.BANK_ACCOUNT.STATE.LOCKED; const shouldShowBankAccount = (!!isBankAccountFullySetup || !!bankAccountConnectedToWorkspace) && policy?.reimbursementChoice !== CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO; + const bankAccountPendingAction = bankAccountConnectedToWorkspace?.pendingAction; const bankIcon = getBankIcon({bankName: bankName as BankName, isCard: false, styles}); @@ -526,7 +527,7 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) { subMenuItems: ( <> {shouldShowBankAccount ? ( - <> + {translate('workflowsPayerPage.paymentAccount')} @@ -572,7 +573,7 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) { wrapperStyle={[styles.sectionMenuItemTopDescription, styles.mt3, styles.mbn3]} brickRoadIndicator={hasReimburserError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} /> - + ) : ( Date: Mon, 25 May 2026 09:11:18 +0000 Subject: [PATCH 3/4] Add unit tests for resetUSDBankAccount and resetNonUSDBankAccount Tests verify that BANK_ACCOUNT_LIST is updated correctly during bank account reset: pendingAction set to DELETE optimistically, entry nulled on success, and pendingAction cleared on failure. Co-authored-by: Eugene Voloshchak --- tests/actions/ReimbursementAccountTest.ts | 170 ++++++++++++++++++++++ 1 file changed, 170 insertions(+) diff --git a/tests/actions/ReimbursementAccountTest.ts b/tests/actions/ReimbursementAccountTest.ts index 7e1a2cbda1f4..eb9eb0fb2e7d 100644 --- a/tests/actions/ReimbursementAccountTest.ts +++ b/tests/actions/ReimbursementAccountTest.ts @@ -1,6 +1,7 @@ import Onyx from 'react-native-onyx'; import CONST from '@src/CONST'; import IntlStore from '@src/languages/IntlStore'; +import resetNonUSDBankAccount from '@src/libs/actions/ReimbursementAccount/resetNonUSDBankAccount'; import resetUSDBankAccount from '@src/libs/actions/ReimbursementAccount/resetUSDBankAccount'; import ONYXKEYS from '@src/ONYXKEYS'; import type {ACHAccount} from '@src/types/onyx/Policy'; @@ -60,5 +61,174 @@ describe('ReimbursementAccount', () => { }), ); }); + + it('should optimistically mark bank account as pending deletion', async () => { + (fetch as MockFetch)?.pause?.(); + const achAccount: ACHAccount = { + bankAccountID, + addressName: 'Test Address', + bankName: 'Test Bank', + reimburser: TEST_EMAIL, + accountNumber: '1234567890', + routingNumber: '123456789', + }; + await Onyx.set(ONYXKEYS.BANK_ACCOUNT_LIST, { + [bankAccountID]: {bankCurrency: 'USD', bankCountry: 'US'}, + }); + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {achAccount}); + resetUSDBankAccount(bankAccountID, session, policyID, achAccount); + + await waitForBatchedUpdates(); + return new Promise((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.BANK_ACCOUNT_LIST, + callback: (bankAccountList) => { + Onyx.disconnect(connection); + expect(bankAccountList?.[bankAccountID]?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); + resolve(); + }, + }); + }); + }); + + it('should remove bank account from list on success', async () => { + const achAccount: ACHAccount = { + bankAccountID, + addressName: 'Test Address', + bankName: 'Test Bank', + reimburser: TEST_EMAIL, + accountNumber: '1234567890', + routingNumber: '123456789', + }; + await Onyx.set(ONYXKEYS.BANK_ACCOUNT_LIST, { + [bankAccountID]: {bankCurrency: 'USD', bankCountry: 'US'}, + }); + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {achAccount}); + resetUSDBankAccount(bankAccountID, session, policyID, achAccount); + + await waitForBatchedUpdates(); + return new Promise((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.BANK_ACCOUNT_LIST, + callback: (bankAccountList) => { + Onyx.disconnect(connection); + expect(bankAccountList?.[bankAccountID]).toBeUndefined(); + resolve(); + }, + }); + }); + }); + }); + + describe('resetNonUSDBankAccount', () => { + afterEach(() => { + mockFetch?.resume?.(); + }); + + it('should optimistically mark bank account as pending deletion', async () => { + (fetch as MockFetch)?.pause?.(); + const achAccount: ACHAccount = { + bankAccountID, + addressName: 'Test Address', + bankName: 'Test Bank', + reimburser: TEST_EMAIL, + accountNumber: '1234567890', + routingNumber: '123456789', + }; + await Onyx.set(ONYXKEYS.BANK_ACCOUNT_LIST, { + [bankAccountID]: {bankCurrency: 'CAD', bankCountry: 'CA'}, + }); + resetNonUSDBankAccount(policyID, achAccount, bankAccountID); + + await waitForBatchedUpdates(); + return new Promise((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.BANK_ACCOUNT_LIST, + callback: (bankAccountList) => { + Onyx.disconnect(connection); + expect(bankAccountList?.[bankAccountID]?.pendingAction).toBe(CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); + resolve(); + }, + }); + }); + }); + + it('should remove bank account from list on success', async () => { + const achAccount: ACHAccount = { + bankAccountID, + addressName: 'Test Address', + bankName: 'Test Bank', + reimburser: TEST_EMAIL, + accountNumber: '1234567890', + routingNumber: '123456789', + }; + await Onyx.set(ONYXKEYS.BANK_ACCOUNT_LIST, { + [bankAccountID]: {bankCurrency: 'CAD', bankCountry: 'CA'}, + }); + resetNonUSDBankAccount(policyID, achAccount, bankAccountID); + + await waitForBatchedUpdates(); + return new Promise((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.BANK_ACCOUNT_LIST, + callback: (bankAccountList) => { + Onyx.disconnect(connection); + expect(bankAccountList?.[bankAccountID]).toBeUndefined(); + resolve(); + }, + }); + }); + }); + + it('should clear policy achAccount optimistically', async () => { + (fetch as MockFetch)?.pause?.(); + const achAccount: ACHAccount = { + bankAccountID, + addressName: 'Test Address', + bankName: 'Test Bank', + reimburser: TEST_EMAIL, + accountNumber: '1234567890', + routingNumber: '123456789', + }; + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {achAccount}); + resetNonUSDBankAccount(policyID, achAccount, bankAccountID); + + await waitForBatchedUpdates(); + return new Promise((resolve) => { + const connection = Onyx.connect({ + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + callback: (policy) => { + Onyx.disconnect(connection); + expect(policy?.achAccount).toBeUndefined(); + resolve(); + }, + }); + }); + }); + + it('should reset locally without API call when no bankAccountID', async () => { + const achAccount: ACHAccount = { + bankAccountID: 0, + addressName: 'Test Address', + bankName: 'Test Bank', + reimburser: TEST_EMAIL, + accountNumber: '1234567890', + routingNumber: '123456789', + }; + await Onyx.set(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {achAccount}); + resetNonUSDBankAccount(policyID, achAccount); + + await waitForBatchedUpdates(); + return new Promise((resolve) => { + const connection = Onyx.connect({ + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + callback: (reimbursementAccount) => { + Onyx.disconnect(connection); + expect(reimbursementAccount).toEqual(CONST.REIMBURSEMENT_ACCOUNT.DEFAULT_DATA); + resolve(); + }, + }); + }); + }); }); }); From 2fa161101bd2a37cadc2e92b9c3df75a9883285a Mon Sep 17 00:00:00 2001 From: "Eugene Voloshchak (via MelvinBot)" Date: Mon, 25 May 2026 09:40:27 +0000 Subject: [PATCH 4/4] Apply strikethrough directly to MenuItem title/description on native OfflineWithFeedback applies textDecorationLine via View style which cascades on web via CSS but is ignored on native RN Views. Pass offlineFeedbackDeleted directly to MenuItem's titleStyle and descriptionTextStyle props so the strikethrough renders on all platforms. Co-authored-by: Eugene Voloshchak --- src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx index 43dde60fe6d2..44ea160f28c2 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx @@ -323,6 +323,7 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) { const shouldShowBankAccount = (!!isBankAccountFullySetup || !!bankAccountConnectedToWorkspace) && policy?.reimbursementChoice !== CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_NO; const bankAccountPendingAction = bankAccountConnectedToWorkspace?.pendingAction; + const isBankAccountPendingDelete = bankAccountPendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; const bankIcon = getBankIcon({bankName: bankName as BankName, isCard: false, styles}); @@ -562,6 +563,8 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) { iconHeight={bankIcon.iconHeight ?? bankIcon.iconSize} iconWidth={bankIcon.iconWidth ?? bankIcon.iconSize} iconStyles={bankIcon.iconStyles} + titleStyle={isBankAccountPendingDelete ? styles.offlineFeedbackDeleted : undefined} + descriptionTextStyle={isBankAccountPendingDelete ? styles.offlineFeedbackDeleted : undefined} disabled={isOffline || !isPolicyAdmin} badgeText={getBadgeText(accountData?.state)} sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.WORKFLOWS.BANK_ACCOUNT}