From 7ba2855131cacb7eaaf2af81da7b359aaff4a74a Mon Sep 17 00:00:00 2001 From: Sergei Sharabai <105950333+sharabai@users.noreply.github.com> Date: Tue, 21 Jul 2026 13:09:39 +0200 Subject: [PATCH 1/8] Add first implementation --- src/ROUTES.ts | 1 + .../ReportActionItem/MoneyRequestView.tsx | 2 - src/languages/en.ts | 4 + src/libs/ReportUtils.ts | 23 ++- .../hooks/useTimeSensitiveAddBankAccount.ts | 94 +++++++++ src/pages/home/TimeSensitiveSection/index.tsx | 41 ++-- .../items/AddBankAccount.tsx | 41 ++++ .../useTimeSensitiveAddBankAccount.test.ts | 179 ++++++++++++++++++ .../AddBankAccountTest.tsx | 122 ++++++++++++ .../ValidateAccountTest.tsx | 7 + 10 files changed, 491 insertions(+), 23 deletions(-) create mode 100644 src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveAddBankAccount.ts create mode 100644 src/pages/home/TimeSensitiveSection/items/AddBankAccount.tsx create mode 100644 tests/unit/hooks/useTimeSensitiveAddBankAccount.test.ts create mode 100644 tests/unit/pages/home/TimeSensitiveSection/AddBankAccountTest.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index f7b9df0bae9d..4eb4621c29a5 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -121,6 +121,7 @@ const DYNAMIC_ROUTES = { path: 'add-bank-account/verify-account', entryScreens: [ SCREENS.SETTINGS.WALLET.ROOT, + SCREENS.HOME, SCREENS.RIGHT_MODAL.SEARCH_REPORT, SCREENS.RIGHT_MODAL.EXPENSE_REPORT, SCREENS.REPORT, diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index b378e40347c2..d1e22eb0be7d 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -878,8 +878,6 @@ function MoneyRequestView({ }); }; - const [reportPolicyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${getNonEmptyStringOnyxID(parentReport?.policyID)}`); - const showTagDisabledAlert = (tagListIndex: number) => { const transactionID = transaction?.transactionID; if (!transactionID) { diff --git a/src/languages/en.ts b/src/languages/en.ts index 9a0da9539e9a..e78221eba5a0 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1026,6 +1026,10 @@ const translations = { subtitle: 'Account > Subscription', cta: 'Add', }, + addBankAccount: { + // @context Home reminder shown to a payment recipient when a reimbursement is waiting for them to add a personal deposit account. + title: 'Add a bank account to get reimbursed', + }, activateCard: { title: 'Activate your Expensify Card', subtitle: 'Validate your card and start spending.', diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 0a6d5e6ca329..27961a56996a 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -11138,6 +11138,22 @@ function isAllowedToApproveExpenseReport(report: OnyxEntry, approverAcco /** * What missing payment method does this report action indicate, if any? */ +function getMissingPaymentMethodForQueuedPayment( + userWalletTierName: string | undefined, + reportAction: ReportAction, + bankAccountList: OnyxEntry, +): MissingPaymentMethod | undefined { + const paymentType = getOriginalMessage(reportAction)?.paymentType; + if (paymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY) { + return !userWalletTierName || userWalletTierName === CONST.WALLET.TIER_NAME.SILVER ? 'wallet' : undefined; + } + + return !hasCreditBankAccount(bankAccountList) ? 'bankAccount' : undefined; +} + +/** + * What missing payment method does this action indicate for the current report submitter, if any? + */ function getIndicatedMissingPaymentMethod( userWalletTierName: string | undefined, reportId: string | undefined, @@ -11148,12 +11164,8 @@ function getIndicatedMissingPaymentMethod( if (!reportId || !isSubmitterOfUnsettledReport || !isReimbursementQueuedAction(reportAction)) { return undefined; } - const paymentType = getOriginalMessage(reportAction)?.paymentType; - if (paymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY) { - return !userWalletTierName || userWalletTierName === CONST.WALLET.TIER_NAME.SILVER ? 'wallet' : undefined; - } - return !hasCreditBankAccount(bankAccountList) ? 'bankAccount' : undefined; + return getMissingPaymentMethodForQueuedPayment(userWalletTierName, reportAction, bankAccountList); } /** @@ -13608,6 +13620,7 @@ export { sortIconsByName, getIconsForParticipants, getIndicatedMissingPaymentMethod, + getMissingPaymentMethodForQueuedPayment, getLastVisibleMessage, getMoneyRequestSpendBreakdown, getNonHeldAndFullAmount, diff --git a/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveAddBankAccount.ts b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveAddBankAccount.ts new file mode 100644 index 000000000000..7cb1d3b00131 --- /dev/null +++ b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveAddBankAccount.ts @@ -0,0 +1,94 @@ +/** + * Identifies queued payments that require the current user to add a personal bank account. + */ +import useOnyx from '@hooks/useOnyx'; + +import hasCreditBankAccount from '@libs/actions/ReimbursementAccount/hasCreditBankAccount'; +import {isNewerReportAction, isReimbursementQueuedAction} from '@libs/ReportActionsUtils'; +import {getMissingPaymentMethodForQueuedPayment} from '@libs/ReportUtils'; + +import type CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Report, ReportAction, ReportActions} from '@src/types/onyx'; +import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; + +import type {OnyxCollection} from 'react-native-onyx'; + +import {accountIDSelector} from '@selectors/Session'; +import {tierNameSelector} from '@selectors/UserWallet'; + +type ReimbursementQueuedAction = ReportAction; + +type WaitingReportPaymentData = { + reportID: string; + chatReportID?: string; + chatIOUReportID?: string; +}; + +function getLatestReimbursementQueuedAction( + reportID: string, + chatReportID: string | undefined, + chatIOUReportID: string | undefined, + allReportActions: OnyxCollection, +): ReimbursementQueuedAction | undefined { + let latestAction: ReimbursementQueuedAction | undefined; + const seenActionIDs = new Set(); + const actionCollections = [ + {actions: allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`], isChatActionCollection: false}, + {actions: chatReportID ? allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`] : undefined, isChatActionCollection: true}, + ]; + + for (const {actions, isChatActionCollection} of actionCollections) { + for (const action of Object.values(actions ?? {})) { + if (!isReimbursementQueuedAction(action)) { + continue; + } + const isCorrelatedByChildReportID = action.childReportID === reportID; + const isCorrelatedByCollection = !action.childReportID && (!isChatActionCollection || chatIOUReportID === reportID); + if (!isCorrelatedByChildReportID && !isCorrelatedByCollection) { + continue; + } + if (seenActionIDs.has(action.reportActionID)) { + continue; + } + seenActionIDs.add(action.reportActionID); + if (!latestAction || isNewerReportAction(action, latestAction)) { + latestAction = action; + } + } + } + + return latestAction; +} + +function useTimeSensitiveAddBankAccount() { + const [accountID] = useOnyx(ONYXKEYS.SESSION, {selector: accountIDSelector}); + const waitingReportsSelector = (allReports: OnyxCollection): WaitingReportPaymentData[] => + Object.values(allReports ?? {}) + .filter((report): report is Report => !!report?.reportID && report.isWaitingOnBankAccount === true && report.ownerAccountID === accountID) + .map((report) => ({ + reportID: report.reportID, + chatReportID: report.chatReportID, + chatIOUReportID: report.chatReportID ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report.chatReportID}`]?.iouReportID : undefined, + })); + const [waitingReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: waitingReportsSelector}); + const [bankAccountList, bankAccountListMetadata] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); + const [userWalletTierName] = useOnyx(ONYXKEYS.USER_WALLET, {selector: tierNameSelector}); + const canShowAddBankAccount = accountID !== undefined && !isLoadingOnyxValue(bankAccountListMetadata) && !hasCreditBankAccount(bankAccountList); + + const shouldShowAddBankAccountSelector = (allReportActions: OnyxCollection): boolean => { + if (!canShowAddBankAccount) { + return false; + } + + return (waitingReports ?? []).some((report) => { + const queuedAction = getLatestReimbursementQueuedAction(report.reportID, report.chatReportID, report.chatIOUReportID, allReportActions); + return !!queuedAction && getMissingPaymentMethodForQueuedPayment(userWalletTierName, queuedAction, bankAccountList) === 'bankAccount'; + }); + }; + const [shouldShowAddBankAccount = false] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS, {selector: shouldShowAddBankAccountSelector}); + + return {shouldShowAddBankAccount}; +} + +export default useTimeSensitiveAddBankAccount; diff --git a/src/pages/home/TimeSensitiveSection/index.tsx b/src/pages/home/TimeSensitiveSection/index.tsx index b02e842254bf..aef9df798b9b 100644 --- a/src/pages/home/TimeSensitiveSection/index.tsx +++ b/src/pages/home/TimeSensitiveSection/index.tsx @@ -29,12 +29,14 @@ import React, {useCallback, useState} from 'react'; import {View} from 'react-native'; import useBrokenDirectCompanyCardFeedsForAdmin from './hooks/useBrokenDirectCompanyCardFeedsForAdmin'; +import useTimeSensitiveAddBankAccount from './hooks/useTimeSensitiveAddBankAccount'; import useTimeSensitiveAddPaymentCard from './hooks/useTimeSensitiveAddPaymentCard'; import useTimeSensitiveBilling from './hooks/useTimeSensitiveBilling'; import useTimeSensitiveCards from './hooks/useTimeSensitiveCards'; import useTimeSensitiveLockedBankAccount from './hooks/useTimeSensitiveLockedBankAccount'; import useTimeSensitiveSignerInfo from './hooks/useTimeSensitiveSignerInfo'; import ActivateCard from './items/ActivateCard'; +import AddBankAccount from './items/AddBankAccount'; import AddPaymentCard from './items/AddPaymentCard'; import AddShippingAddress from './items/AddShippingAddress'; import AddVirtualCardPersonalDetails from './items/AddVirtualCardPersonalDetails'; @@ -82,6 +84,7 @@ function TimeSensitiveSection() { // Use custom hooks for offers and cards (Release 3) const {shouldShowAddPaymentCard} = useTimeSensitiveAddPaymentCard(); + const {shouldShowAddBankAccount} = useTimeSensitiveAddBankAccount(); const { shouldShowAddShippingAddress, shouldShowActivateCard, @@ -167,6 +170,7 @@ function TimeSensitiveSection() { shouldShowFixFailedBilling || shouldShowReviewCardFraud || shouldShowAddPaymentCard || + shouldShowAddBankAccount || hasBrokenCompanyCards || hasBrokenPersonalCards || hasBrokenPolicyConnections || @@ -183,14 +187,15 @@ function TimeSensitiveSection() { // 2. Fix failed billing (existing customers with declined cards) // 3. Potential card fraud // 4. Add payment card (trial ended, no payment card) - // 5. Broken bank connections (company cards) - // 6. Broken bank connections (personal cards) - // 7. Locked bank accounts (workspace VBAs and personal) - // 8. Enter signer info for global bank accounts - // 9. Broken policy connections (accounting + HR) - // 10. Expensify card shipping - // 11. Expensify card activation - // 12. Virtual Expensify card needs personal details + // 5. Add bank account for a queued reimbursement + // 6. Broken bank connections (company cards) + // 7. Broken bank connections (personal cards) + // 8. Locked bank accounts (workspace VBAs and personal) + // 9. Enter signer info for global bank accounts + // 10. Broken policy connections (accounting + HR) + // 11. Expensify card shipping + // 12. Expensify card activation + // 13. Virtual Expensify card needs personal details const items: React.ReactNode[] = []; // Priority 1: Validate account @@ -219,7 +224,11 @@ function TimeSensitiveSection() { if (shouldShowAddPaymentCard) { items.push(); } - // Priority 5: Broken company card connections + // Priority 5: Add bank account for a queued reimbursement + if (shouldShowAddBankAccount) { + items.push(); + } + // Priority 6: Broken company card connections for (const connection of brokenCompanyCardConnections) { const card = cardFeedErrors.cardsWithBrokenFeedConnection[connection.cardID]; if (!card) { @@ -234,7 +243,7 @@ function TimeSensitiveSection() { />, ); } - // Priority 6: Broken personal card connections + // Priority 7: Broken personal card connections for (const connection of brokenPersonalCardConnections) { const card = cardFeedErrors.personalCardsWithBrokenConnection[connection.cardID]; if (!card) { @@ -247,7 +256,7 @@ function TimeSensitiveSection() { />, ); } - // Priority 7: Locked bank accounts + // Priority 8: Locked bank accounts for (const lockedBankAccount of lockedBankAccounts) { items.push( , ); } - // Priority 8: Enter signer info for global bank accounts + // Priority 9: Enter signer info for global bank accounts for (const item of pendingSignerInfo) { items.push( , ); } - // Priority 9: Broken policy connections (accounting + HR) + // Priority 10: Broken policy connections (accounting + HR) for (const connection of brokenPolicyConnections) { items.push( , ); } - // Priority 10: Expensify card shipping + // Priority 11: Expensify card shipping if (shouldShowAddShippingAddress) { for (const card of cardsNeedingShippingAddress) { items.push( @@ -291,7 +300,7 @@ function TimeSensitiveSection() { ); } } - // Priority 11: Expensify card activation + // Priority 12: Expensify card activation if (shouldShowActivateCard) { for (const card of cardsNeedingActivation) { items.push( @@ -302,7 +311,7 @@ function TimeSensitiveSection() { ); } } - // Priority 11: Virtual Expensify card needs personal details before reveal + // Priority 13: Virtual Expensify card needs personal details before reveal if (shouldShowAddVirtualCardPersonalDetails) { for (const card of virtualCardsNeedingPersonalDetails) { items.push( diff --git a/src/pages/home/TimeSensitiveSection/items/AddBankAccount.tsx b/src/pages/home/TimeSensitiveSection/items/AddBankAccount.tsx new file mode 100644 index 000000000000..218e4d1c4034 --- /dev/null +++ b/src/pages/home/TimeSensitiveSection/items/AddBankAccount.tsx @@ -0,0 +1,41 @@ +/** + * Home widget that opens personal bank-account setup for a queued reimbursement. + */ +import BaseWidgetItem from '@components/BaseWidgetItem'; + +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; + +import {openPersonalBankAccountSetupView} from '@libs/actions/BankAccounts'; + +import colors from '@styles/theme/colors'; + +import ONYXKEYS from '@src/ONYXKEYS'; + +import {isUserValidatedSelector} from '@selectors/Account'; +import React from 'react'; + +function AddBankAccount() { + const {translate} = useLocalize(); + const icons = useMemoizedLazyExpensifyIcons(['Bank']); + const [isUserValidated] = useOnyx(ONYXKEYS.ACCOUNT, {selector: isUserValidatedSelector}); + const title = translate('homePage.timeSensitiveSection.addBankAccount.title'); + const subtitle = translate('common.wallet'); + const ctaText = translate('common.add'); + + return ( + openPersonalBankAccountSetupView({isUserValidated})} + buttonProps={{success: true}} + /> + ); +} + +export default AddBankAccount; diff --git a/tests/unit/hooks/useTimeSensitiveAddBankAccount.test.ts b/tests/unit/hooks/useTimeSensitiveAddBankAccount.test.ts new file mode 100644 index 000000000000..56d64a18f92e --- /dev/null +++ b/tests/unit/hooks/useTimeSensitiveAddBankAccount.test.ts @@ -0,0 +1,179 @@ +import {renderHook, waitFor} from '@testing-library/react-native'; + +import useTimeSensitiveAddBankAccount from '@pages/home/TimeSensitiveSection/hooks/useTimeSensitiveAddBankAccount'; + +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {BankAccount, Report, ReportAction} from '@src/types/onyx'; + +import Onyx from 'react-native-onyx'; + +import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; + +const ACCOUNT_ID = 1; +const OTHER_ACCOUNT_ID = 2; +const REPORT_ID = '1'; +const CHAT_REPORT_ID = '2'; + +function makeWaitingReport(reportID: string, ownerAccountID = ACCOUNT_ID, chatReportID = CHAT_REPORT_ID): Report { + return { + reportID, + ownerAccountID, + chatReportID, + isWaitingOnBankAccount: true, + } as Report; +} + +function makeChatReport(iouReportID: string): Report { + return { + reportID: CHAT_REPORT_ID, + iouReportID, + } as Report; +} + +function makeQueuedAction( + reportActionID: string, + paymentType: typeof CONST.IOU.PAYMENT_TYPE.VBBA | typeof CONST.IOU.PAYMENT_TYPE.EXPENSIFY, + created: string, + childReportID?: string, +): ReportAction { + return { + reportActionID, + actionName: CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENT_QUEUED, + created, + childReportID, + originalMessage: {paymentType}, + }; +} + +async function setCurrentUserPaymentState() { + await Onyx.set(ONYXKEYS.SESSION, {accountID: ACCOUNT_ID}); + await Onyx.set(ONYXKEYS.BANK_ACCOUNT_LIST, {}); + await Onyx.set(ONYXKEYS.USER_WALLET, {tierName: CONST.WALLET.TIER_NAME.SILVER}); + await waitForBatchedUpdates(); +} + +describe('useTimeSensitiveAddBankAccount', () => { + beforeAll(() => { + Onyx.init({keys: ONYXKEYS}); + }); + + beforeEach(async () => { + await Onyx.clear(); + await setCurrentUserPaymentState(); + }); + + afterEach(async () => { + await Onyx.clear(); + }); + + it('reacts when an ACH action without a childReportID arrives in the waiting report collection', async () => { + const {result} = renderHook(() => useTimeSensitiveAddBankAccount()); + + expect(result.current.shouldShowAddBankAccount).toBe(false); + + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, makeWaitingReport(REPORT_ID)); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { + action1: makeQueuedAction('action1', CONST.IOU.PAYMENT_TYPE.VBBA, '2026-07-20 10:00:00.000'), + }); + + await waitFor(() => expect(result.current.shouldShowAddBankAccount).toBe(true)); + }); + + it('does not show for a Wallet payment', async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, makeWaitingReport(REPORT_ID)); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { + action1: makeQueuedAction('action1', CONST.IOU.PAYMENT_TYPE.EXPENSIFY, '2026-07-20 10:00:00.000'), + }); + + const {result} = renderHook(() => useTimeSensitiveAddBankAccount()); + + expect(result.current.shouldShowAddBankAccount).toBe(false); + }); + + it('uses the latest queued action when a report has multiple payment attempts', async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, makeWaitingReport(REPORT_ID)); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { + bankAction: makeQueuedAction('bankAction', CONST.IOU.PAYMENT_TYPE.VBBA, '2026-07-20 10:00:00.000'), + walletAction: makeQueuedAction('walletAction', CONST.IOU.PAYMENT_TYPE.EXPENSIFY, '2026-07-20 10:01:00.000'), + }); + + const {result} = renderHook(() => useTimeSensitiveAddBankAccount()); + + expect(result.current.shouldShowAddBankAccount).toBe(false); + }); + + it('does not show for another user waiting on a bank account', async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, makeWaitingReport(REPORT_ID, OTHER_ACCOUNT_ID)); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { + action1: makeQueuedAction('action1', CONST.IOU.PAYMENT_TYPE.VBBA, '2026-07-20 10:00:00.000'), + }); + + const {result} = renderHook(() => useTimeSensitiveAddBankAccount()); + + expect(result.current.shouldShowAddBankAccount).toBe(false); + }); + + it('does not show when the user already has a default credit bank account', async () => { + const bankAccount: BankAccount = { + bankCurrency: CONST.CURRENCY.USD, + bankCountry: CONST.COUNTRY.US, + accountData: {defaultCredit: true}, + }; + await Onyx.set(ONYXKEYS.BANK_ACCOUNT_LIST, {bankAccount}); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, makeWaitingReport(REPORT_ID)); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, { + action1: makeQueuedAction('action1', CONST.IOU.PAYMENT_TYPE.VBBA, '2026-07-20 10:00:00.000'), + }); + + const {result} = renderHook(() => useTimeSensitiveAddBankAccount()); + + expect(result.current.shouldShowAddBankAccount).toBe(false); + }); + + it("uses a parent-chat action without a childReportID only when the chat's IOU report matches", async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, makeWaitingReport(REPORT_ID)); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${CHAT_REPORT_ID}`, makeChatReport('anotherReport')); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${CHAT_REPORT_ID}`, { + action1: makeQueuedAction('action1', CONST.IOU.PAYMENT_TYPE.VBBA, '2026-07-20 10:00:00.000'), + }); + const {result} = renderHook(() => useTimeSensitiveAddBankAccount()); + + expect(result.current.shouldShowAddBankAccount).toBe(false); + + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${CHAT_REPORT_ID}`, makeChatReport(REPORT_ID)); + + await waitFor(() => expect(result.current.shouldShowAddBankAccount).toBe(true)); + }); + + it('uses a parent-chat action whose childReportID matches the waiting report', async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, makeWaitingReport(REPORT_ID)); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${CHAT_REPORT_ID}`, { + action1: makeQueuedAction('action1', CONST.IOU.PAYMENT_TYPE.VBBA, '2026-07-20 10:00:00.000', REPORT_ID), + }); + + const {result} = renderHook(() => useTimeSensitiveAddBankAccount()); + + expect(result.current.shouldShowAddBankAccount).toBe(true); + }); + + it('continues past a Wallet wait when another report is waiting for a bank account', async () => { + const secondReportID = '3'; + await Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT, { + [`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`]: makeWaitingReport(REPORT_ID), + [`${ONYXKEYS.COLLECTION.REPORT}${secondReportID}`]: makeWaitingReport(secondReportID, ACCOUNT_ID, '4'), + }); + await Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT_ACTIONS, { + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`]: { + walletAction: makeQueuedAction('walletAction', CONST.IOU.PAYMENT_TYPE.EXPENSIFY, '2026-07-20 10:00:00.000'), + }, + [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${secondReportID}`]: { + bankAction: makeQueuedAction('bankAction', CONST.IOU.PAYMENT_TYPE.VBBA, '2026-07-20 10:01:00.000'), + }, + }); + + const {result} = renderHook(() => useTimeSensitiveAddBankAccount()); + + expect(result.current.shouldShowAddBankAccount).toBe(true); + }); +}); diff --git a/tests/unit/pages/home/TimeSensitiveSection/AddBankAccountTest.tsx b/tests/unit/pages/home/TimeSensitiveSection/AddBankAccountTest.tsx new file mode 100644 index 000000000000..76ec13efd894 --- /dev/null +++ b/tests/unit/pages/home/TimeSensitiveSection/AddBankAccountTest.tsx @@ -0,0 +1,122 @@ +import {fireEvent, render, screen} from '@testing-library/react-native'; + +import OnyxListItemProvider from '@src/components/OnyxListItemProvider'; +import {openPersonalBankAccountSetupView} from '@src/libs/actions/BankAccounts'; +import ONYXKEYS from '@src/ONYXKEYS'; +import TimeSensitiveSection from '@src/pages/home/TimeSensitiveSection'; +import useTimeSensitiveAddBankAccount from '@src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveAddBankAccount'; +import useTimeSensitiveAddPaymentCard from '@src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveAddPaymentCard'; + +import type * as NativeNavigation from '@react-navigation/native'; + +import Onyx from 'react-native-onyx'; + +import waitForBatchedUpdates from '../../../../utils/waitForBatchedUpdates'; + +jest.mock('@libs/Navigation/Navigation'); +jest.mock('@src/libs/actions/BankAccounts', () => ({ + openPersonalBankAccountSetupView: jest.fn(), +})); + +jest.mock('@react-navigation/native', () => ({ + ...jest.requireActual('@react-navigation/native'), + useFocusEffect: jest.fn(), +})); + +jest.mock('@hooks/useLocalize', () => jest.fn(() => ({translate: jest.fn((key: string) => key)}))); + +jest.mock('@hooks/useLazyAsset', () => ({ + useMemoizedLazyExpensifyIcons: jest.fn(() => ({ + Bank: () => null, + })), +})); + +jest.mock('@src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveAddBankAccount', () => + jest.fn(() => ({ + shouldShowAddBankAccount: true, + })), +); + +jest.mock('@src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveAddPaymentCard', () => + jest.fn(() => ({ + shouldShowAddPaymentCard: false, + })), +); + +jest.mock('@src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveCards', () => + jest.fn(() => ({ + shouldShowAddShippingAddress: false, + shouldShowActivateCard: false, + shouldShowReviewCardFraud: false, + shouldShowAddVirtualCardPersonalDetails: false, + cardsNeedingShippingAddress: [], + cardsNeedingActivation: [], + cardsWithFraud: [], + virtualCardsNeedingPersonalDetails: [], + })), +); + +jest.mock('@hooks/useCardFeedErrors', () => + jest.fn(() => ({ + cardsWithBrokenFeedConnection: {}, + personalCardsWithBrokenConnection: {}, + })), +); + +jest.mock('@hooks/useCurrentUserPersonalDetails', () => jest.fn(() => ({login: 'test@example.com'}))); + +jest.mock('@hooks/useResponsiveLayout', () => jest.fn(() => ({shouldUseNarrowLayout: false}))); + +const renderTimeSensitiveSection = () => + render( + + + , + ); + +describe('TimeSensitiveSection - AddBankAccount', () => { + const mockedUseTimeSensitiveAddBankAccount = jest.mocked(useTimeSensitiveAddBankAccount); + const mockedUseTimeSensitiveAddPaymentCard = jest.mocked(useTimeSensitiveAddPaymentCard); + + beforeAll(() => { + Onyx.init({keys: ONYXKEYS}); + }); + + beforeEach(async () => { + jest.clearAllMocks(); + mockedUseTimeSensitiveAddBankAccount.mockReturnValue({ + shouldShowAddBankAccount: true, + }); + mockedUseTimeSensitiveAddPaymentCard.mockReturnValue({ + shouldShowAddPaymentCard: false, + }); + await Onyx.clear(); + await waitForBatchedUpdates(); + }); + + afterEach(async () => { + await Onyx.clear(); + }); + + it('renders when it is the only time-sensitive item and starts setup', async () => { + await Onyx.set(ONYXKEYS.ACCOUNT, {validated: true}); + await waitForBatchedUpdates(); + + renderTimeSensitiveSection(); + fireEvent.press(screen.getByText('common.add')); + + expect(screen.getByText('homePage.timeSensitiveSection.title')).toBeTruthy(); + expect(screen.getByText('homePage.timeSensitiveSection.addBankAccount.title')).toBeTruthy(); + expect(openPersonalBankAccountSetupView).toHaveBeenCalledWith({isUserValidated: true}); + }); + + it('passes the unvalidated state to the setup flow', async () => { + await Onyx.set(ONYXKEYS.ACCOUNT, {validated: false}); + await waitForBatchedUpdates(); + + renderTimeSensitiveSection(); + fireEvent.press(screen.getByText('common.add')); + + expect(openPersonalBankAccountSetupView).toHaveBeenCalledWith({isUserValidated: false}); + }); +}); diff --git a/tests/unit/pages/home/TimeSensitiveSection/ValidateAccountTest.tsx b/tests/unit/pages/home/TimeSensitiveSection/ValidateAccountTest.tsx index 16f788e7555f..1eb782060cb8 100644 --- a/tests/unit/pages/home/TimeSensitiveSection/ValidateAccountTest.tsx +++ b/tests/unit/pages/home/TimeSensitiveSection/ValidateAccountTest.tsx @@ -27,6 +27,12 @@ jest.mock('@hooks/useLazyAsset', () => ({ })), })); +jest.mock('@src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveAddBankAccount', () => + jest.fn(() => ({ + shouldShowAddBankAccount: false, + })), +); + jest.mock('@src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveAddPaymentCard', () => jest.fn(() => ({ shouldShowAddPaymentCard: false, @@ -70,6 +76,7 @@ describe('TimeSensitiveSection - ValidateAccount', () => { }); beforeEach(async () => { + jest.clearAllMocks(); mockedUseTimeSensitiveAddPaymentCard.mockReturnValue({ shouldShowAddPaymentCard: false, }); From 1fd75352838799b84511326bbf144325741c2a85 Mon Sep 17 00:00:00 2001 From: Sergei Sharabai <105950333+sharabai@users.noreply.github.com> Date: Tue, 21 Jul 2026 14:25:56 +0200 Subject: [PATCH 2/8] Add translations --- src/languages/de.ts | 3 +++ src/languages/es.ts | 3 +++ src/languages/fr.ts | 3 +++ src/languages/it.ts | 3 +++ src/languages/ja.ts | 3 +++ src/languages/nl.ts | 3 +++ src/languages/pl.ts | 3 +++ src/languages/pt-BR.ts | 3 +++ src/languages/zh-hans.ts | 3 +++ 9 files changed, 27 insertions(+) diff --git a/src/languages/de.ts b/src/languages/de.ts index 38a5c417ff7d..cd7a0ad41325 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -964,6 +964,9 @@ const translations: TranslationDeepObject = { title: 'Zeitkritisch', addShippingAddress: {title: 'Wir benötigen deine Versandadresse', subtitle: 'Geben Sie eine Adresse an, um Ihre Expensify Karte zu erhalten.', cta: 'Adresse hinzufügen'}, addPaymentCard: {title: 'Fügen Sie eine Zahlungskarte hinzu, um Expensify weiter zu nutzen', subtitle: 'Konto > Abonnement', cta: 'Hinzufügen'}, + addBankAccount: { + title: 'Fügen Sie ein Bankkonto hinzu, um Rückerstattungen zu erhalten', + }, activateCard: {title: 'Aktivieren Sie Ihre Expensify Karte', subtitle: 'Validieren Sie Ihre Karte und beginnen Sie mit dem Ausgeben.', cta: 'Aktivieren'}, reviewCardFraud: { title: 'Möglichen Betrug mit Ihrer Expensify Karte überprüfen', diff --git a/src/languages/es.ts b/src/languages/es.ts index 2e2751fa1254..1b0bc7478786 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -929,6 +929,9 @@ const translations: TranslationDeepObject = { subtitle: 'Cuenta > Suscripción', cta: 'Añadir', }, + addBankAccount: { + title: 'Añade una cuenta bancaria para recibir tus reembolsos', + }, activateCard: { title: 'Activa tu Tarjeta Expensify', subtitle: 'Valida tu tarjeta y empieza a gastar.', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index c629abc9b71a..50252f59ad75 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -967,6 +967,9 @@ const translations: TranslationDeepObject = { title: 'Urgent', addShippingAddress: {title: 'Nous avons besoin de votre adresse de livraison', subtitle: 'Indiquez une adresse pour recevoir votre Carte Expensify.', cta: 'Ajouter une adresse'}, addPaymentCard: {title: 'Ajoutez une carte de paiement pour continuer à utiliser Expensify', subtitle: 'Compte > Abonnement', cta: 'Ajouter'}, + addBankAccount: { + title: 'Ajoutez un compte bancaire pour recevoir vos remboursements', + }, activateCard: {title: 'Activer votre Carte Expensify', subtitle: 'Validez votre carte et commencez à dépenser.', cta: 'Activer'}, reviewCardFraud: { title: 'Examiner une éventuelle fraude sur votre Carte Expensify', diff --git a/src/languages/it.ts b/src/languages/it.ts index 6d0e525d96f0..a97e1ee68516 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -965,6 +965,9 @@ const translations: TranslationDeepObject = { title: 'Sensibile al tempo', addShippingAddress: {title: 'Ci serve il tuo indirizzo di spedizione', subtitle: 'Fornisci un indirizzo per ricevere la tua Carta Expensify.', cta: 'Aggiungi indirizzo'}, addPaymentCard: {title: 'Aggiungi una carta di pagamento per continuare a usare Expensify', subtitle: 'Account > Abbonamento', cta: 'Aggiungi'}, + addBankAccount: { + title: 'Aggiungi un conto bancario per ricevere i rimborsi', + }, activateCard: {title: 'Attiva la tua Carta Expensify', subtitle: 'Convalida la tua carta e inizia a spendere.', cta: 'Attiva'}, reviewCardFraud: { title: 'Esamina una possibile frode sulla tua Carta Expensify', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index fa6dd6c61c46..2543093b7dea 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -954,6 +954,9 @@ const translations: TranslationDeepObject = { title: '時間に敏感', addShippingAddress: {title: '配送先住所が必要です', subtitle: 'Expensify カードを受け取る住所を入力してください。', cta: '住所を追加'}, addPaymentCard: {title: 'Expensify を引き続きご利用いただくには、支払いカードを追加してください', subtitle: 'アカウント > サブスクリプション', cta: '追加'}, + addBankAccount: { + title: '払い戻しを受け取るために銀行口座を追加してください', + }, activateCard: {title: 'Expensify カードを有効化する', subtitle: 'カードを認証して支出を始めましょう。', cta: '有効化'}, reviewCardFraud: { title: 'Expensify カードの不正利用の可能性を確認する', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 48358b1a0428..804a5f077293 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -963,6 +963,9 @@ const translations: TranslationDeepObject = { title: 'Tijdgevoelig', addShippingAddress: {title: 'We hebben je verzendadres nodig', subtitle: 'Geef een adres op om je Expensify Kaart te ontvangen.', cta: 'Adres toevoegen'}, addPaymentCard: {title: 'Voeg een betaalkaart toe om Expensify te blijven gebruiken', subtitle: 'Account > Abonnement', cta: 'Toevoegen'}, + addBankAccount: { + title: 'Voeg een bankrekening toe om vergoedingen te ontvangen', + }, activateCard: {title: 'Activeer je Expensify Kaart', subtitle: 'Valideer je kaart en begin met uitgeven.', cta: 'Activeren'}, reviewCardFraud: { title: 'Controleer mogelijk misbruik van je Expensify Kaart', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 15aeecbc3554..1123d54abb8d 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -965,6 +965,9 @@ const translations: TranslationDeepObject = { title: 'Wymaga szybkiej reakcji', addShippingAddress: {title: 'Potrzebujemy Twojego adresu wysyłki', subtitle: 'Podaj adres, na który mamy wysłać twoją Kartę Expensify.', cta: 'Dodaj adres'}, addPaymentCard: {title: 'Dodaj kartę płatniczą, żeby dalej korzystać z Expensify', subtitle: 'Konto > Subskrypcja', cta: 'Dodaj'}, + addBankAccount: { + title: 'Dodaj konto bankowe, aby otrzymywać zwroty', + }, activateCard: {title: 'Aktywuj swoją Kartę Expensify', subtitle: 'Zatwierdź swoją kartę i zacznij wydawać.', cta: 'Aktywuj'}, reviewCardFraud: { title: 'Sprawdź potencjalne oszustwo na swojej Karcie Expensify', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index e4c7ecd38a2e..815e16930eb1 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -963,6 +963,9 @@ const translations: TranslationDeepObject = { title: 'Urgente', addShippingAddress: {title: 'Precisamos do seu endereço de entrega', subtitle: 'Informe um endereço para receber seu Cartão Expensify.', cta: 'Adicionar endereço'}, addPaymentCard: {title: 'Adicione um cartão de pagamento para continuar usando o Expensify', subtitle: 'Conta > Assinatura', cta: 'Adicionar'}, + addBankAccount: { + title: 'Adicione uma conta bancária para receber reembolsos', + }, activateCard: {title: 'Ative seu Cartão Expensify', subtitle: 'Valide seu cartão e comece a gastar.', cta: 'Ativar'}, reviewCardFraud: { title: 'Analisar possível fraude no seu Cartão Expensify', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 99fdc6e70841..06ebf221ab85 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -937,6 +937,9 @@ const translations: TranslationDeepObject = { title: '时间敏感', addShippingAddress: {title: '我们需要您的收货地址', subtitle: '请提供一个地址以接收您的 Expensify 卡。', cta: '添加地址'}, addPaymentCard: {title: '添加支付卡以继续使用 Expensify', subtitle: '账户 > 订阅', cta: '添加'}, + addBankAccount: { + title: '添加银行账户以便接收报销款', + }, activateCard: {title: '激活你的 Expensify 卡', subtitle: '验证您的银行卡并开始消费。', cta: '启用'}, reviewCardFraud: { title: '审查您 Expensify 卡上的潜在欺诈交易', From 1545cb5d9fdc7608c458e15bc8168782af032823 Mon Sep 17 00:00:00 2001 From: Sergei Sharabai <105950333+sharabai@users.noreply.github.com> Date: Thu, 23 Jul 2026 18:00:57 +0200 Subject: [PATCH 3/8] Simplify hook and update tests --- .../hooks/useTimeSensitiveAddBankAccount.ts | 39 +++--------------- .../useTimeSensitiveAddBankAccount.test.ts | 41 +------------------ 2 files changed, 8 insertions(+), 72 deletions(-) diff --git a/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveAddBankAccount.ts b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveAddBankAccount.ts index 7cb1d3b00131..2bdfa55ffa6d 100644 --- a/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveAddBankAccount.ts +++ b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveAddBankAccount.ts @@ -21,40 +21,15 @@ type ReimbursementQueuedAction = ReportAction, -): ReimbursementQueuedAction | undefined { +function getLatestReimbursementQueuedAction(reportID: string, allReportActions: OnyxCollection): ReimbursementQueuedAction | undefined { let latestAction: ReimbursementQueuedAction | undefined; - const seenActionIDs = new Set(); - const actionCollections = [ - {actions: allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`], isChatActionCollection: false}, - {actions: chatReportID ? allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`] : undefined, isChatActionCollection: true}, - ]; + const reportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]; - for (const {actions, isChatActionCollection} of actionCollections) { - for (const action of Object.values(actions ?? {})) { - if (!isReimbursementQueuedAction(action)) { - continue; - } - const isCorrelatedByChildReportID = action.childReportID === reportID; - const isCorrelatedByCollection = !action.childReportID && (!isChatActionCollection || chatIOUReportID === reportID); - if (!isCorrelatedByChildReportID && !isCorrelatedByCollection) { - continue; - } - if (seenActionIDs.has(action.reportActionID)) { - continue; - } - seenActionIDs.add(action.reportActionID); - if (!latestAction || isNewerReportAction(action, latestAction)) { - latestAction = action; - } + for (const action of Object.values(reportActions ?? {})) { + if (isReimbursementQueuedAction(action) && (!latestAction || isNewerReportAction(action, latestAction))) { + latestAction = action; } } @@ -68,8 +43,6 @@ function useTimeSensitiveAddBankAccount() { .filter((report): report is Report => !!report?.reportID && report.isWaitingOnBankAccount === true && report.ownerAccountID === accountID) .map((report) => ({ reportID: report.reportID, - chatReportID: report.chatReportID, - chatIOUReportID: report.chatReportID ? allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report.chatReportID}`]?.iouReportID : undefined, })); const [waitingReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: waitingReportsSelector}); const [bankAccountList, bankAccountListMetadata] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); @@ -82,7 +55,7 @@ function useTimeSensitiveAddBankAccount() { } return (waitingReports ?? []).some((report) => { - const queuedAction = getLatestReimbursementQueuedAction(report.reportID, report.chatReportID, report.chatIOUReportID, allReportActions); + const queuedAction = getLatestReimbursementQueuedAction(report.reportID, allReportActions); return !!queuedAction && getMissingPaymentMethodForQueuedPayment(userWalletTierName, queuedAction, bankAccountList) === 'bankAccount'; }); }; diff --git a/tests/unit/hooks/useTimeSensitiveAddBankAccount.test.ts b/tests/unit/hooks/useTimeSensitiveAddBankAccount.test.ts index 56d64a18f92e..0a79ddd4506d 100644 --- a/tests/unit/hooks/useTimeSensitiveAddBankAccount.test.ts +++ b/tests/unit/hooks/useTimeSensitiveAddBankAccount.test.ts @@ -13,35 +13,24 @@ import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates'; const ACCOUNT_ID = 1; const OTHER_ACCOUNT_ID = 2; const REPORT_ID = '1'; -const CHAT_REPORT_ID = '2'; -function makeWaitingReport(reportID: string, ownerAccountID = ACCOUNT_ID, chatReportID = CHAT_REPORT_ID): Report { +function makeWaitingReport(reportID: string, ownerAccountID = ACCOUNT_ID): Report { return { reportID, ownerAccountID, - chatReportID, isWaitingOnBankAccount: true, } as Report; } -function makeChatReport(iouReportID: string): Report { - return { - reportID: CHAT_REPORT_ID, - iouReportID, - } as Report; -} - function makeQueuedAction( reportActionID: string, paymentType: typeof CONST.IOU.PAYMENT_TYPE.VBBA | typeof CONST.IOU.PAYMENT_TYPE.EXPENSIFY, created: string, - childReportID?: string, ): ReportAction { return { reportActionID, actionName: CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENT_QUEUED, created, - childReportID, originalMessage: {paymentType}, }; } @@ -131,37 +120,11 @@ describe('useTimeSensitiveAddBankAccount', () => { expect(result.current.shouldShowAddBankAccount).toBe(false); }); - it("uses a parent-chat action without a childReportID only when the chat's IOU report matches", async () => { - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, makeWaitingReport(REPORT_ID)); - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${CHAT_REPORT_ID}`, makeChatReport('anotherReport')); - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${CHAT_REPORT_ID}`, { - action1: makeQueuedAction('action1', CONST.IOU.PAYMENT_TYPE.VBBA, '2026-07-20 10:00:00.000'), - }); - const {result} = renderHook(() => useTimeSensitiveAddBankAccount()); - - expect(result.current.shouldShowAddBankAccount).toBe(false); - - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${CHAT_REPORT_ID}`, makeChatReport(REPORT_ID)); - - await waitFor(() => expect(result.current.shouldShowAddBankAccount).toBe(true)); - }); - - it('uses a parent-chat action whose childReportID matches the waiting report', async () => { - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, makeWaitingReport(REPORT_ID)); - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${CHAT_REPORT_ID}`, { - action1: makeQueuedAction('action1', CONST.IOU.PAYMENT_TYPE.VBBA, '2026-07-20 10:00:00.000', REPORT_ID), - }); - - const {result} = renderHook(() => useTimeSensitiveAddBankAccount()); - - expect(result.current.shouldShowAddBankAccount).toBe(true); - }); - it('continues past a Wallet wait when another report is waiting for a bank account', async () => { const secondReportID = '3'; await Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT, { [`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`]: makeWaitingReport(REPORT_ID), - [`${ONYXKEYS.COLLECTION.REPORT}${secondReportID}`]: makeWaitingReport(secondReportID, ACCOUNT_ID, '4'), + [`${ONYXKEYS.COLLECTION.REPORT}${secondReportID}`]: makeWaitingReport(secondReportID), }); await Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT_ACTIONS, { [`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`]: { From edbc5d1692aa4b8509686efa935a58d74fb0c3ec Mon Sep 17 00:00:00 2001 From: Sergei Sharabai <105950333+sharabai@users.noreply.github.com> Date: Thu, 23 Jul 2026 18:12:40 +0200 Subject: [PATCH 4/8] Simplify some more --- .../hooks/useTimeSensitiveAddBankAccount.ts | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveAddBankAccount.ts b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveAddBankAccount.ts index 2bdfa55ffa6d..c90576af12a0 100644 --- a/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveAddBankAccount.ts +++ b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveAddBankAccount.ts @@ -19,10 +19,6 @@ import {tierNameSelector} from '@selectors/UserWallet'; type ReimbursementQueuedAction = ReportAction; -type WaitingReportPaymentData = { - reportID: string; -}; - function getLatestReimbursementQueuedAction(reportID: string, allReportActions: OnyxCollection): ReimbursementQueuedAction | undefined { let latestAction: ReimbursementQueuedAction | undefined; const reportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]; @@ -38,13 +34,11 @@ function getLatestReimbursementQueuedAction(reportID: string, allReportActions: function useTimeSensitiveAddBankAccount() { const [accountID] = useOnyx(ONYXKEYS.SESSION, {selector: accountIDSelector}); - const waitingReportsSelector = (allReports: OnyxCollection): WaitingReportPaymentData[] => + const waitingReportIDsSelector = (allReports: OnyxCollection): string[] => Object.values(allReports ?? {}) .filter((report): report is Report => !!report?.reportID && report.isWaitingOnBankAccount === true && report.ownerAccountID === accountID) - .map((report) => ({ - reportID: report.reportID, - })); - const [waitingReports] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: waitingReportsSelector}); + .map((report) => report.reportID); + const [waitingReportIDs] = useOnyx(ONYXKEYS.COLLECTION.REPORT, {selector: waitingReportIDsSelector}); const [bankAccountList, bankAccountListMetadata] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); const [userWalletTierName] = useOnyx(ONYXKEYS.USER_WALLET, {selector: tierNameSelector}); const canShowAddBankAccount = accountID !== undefined && !isLoadingOnyxValue(bankAccountListMetadata) && !hasCreditBankAccount(bankAccountList); @@ -54,8 +48,8 @@ function useTimeSensitiveAddBankAccount() { return false; } - return (waitingReports ?? []).some((report) => { - const queuedAction = getLatestReimbursementQueuedAction(report.reportID, allReportActions); + return (waitingReportIDs ?? []).some((reportID) => { + const queuedAction = getLatestReimbursementQueuedAction(reportID, allReportActions); return !!queuedAction && getMissingPaymentMethodForQueuedPayment(userWalletTierName, queuedAction, bankAccountList) === 'bankAccount'; }); }; From 89c755aa468e1c885aff9ac331a8029643538952 Mon Sep 17 00:00:00 2001 From: Sergei Sharabai <105950333+sharabai@users.noreply.github.com> Date: Thu, 23 Jul 2026 19:45:36 +0200 Subject: [PATCH 5/8] Fix RHP not closing after adding bank --- src/pages/AddPersonalBankAccountPage.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/AddPersonalBankAccountPage.tsx b/src/pages/AddPersonalBankAccountPage.tsx index 7ad564857e61..d7c165a2b27e 100644 --- a/src/pages/AddPersonalBankAccountPage.tsx +++ b/src/pages/AddPersonalBankAccountPage.tsx @@ -9,6 +9,7 @@ import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; +import getActiveTabName from '@libs/Navigation/helpers/getActiveTabName'; import {isFullScreenName} from '@libs/Navigation/helpers/isNavigatorName'; import Navigation, {navigationRef} from '@libs/Navigation/Navigation'; @@ -18,6 +19,7 @@ import {continueSetup} from '@userActions/PaymentMethods'; import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import SCREENS from '@src/SCREENS'; import React, {useContext, useEffect} from 'react'; @@ -29,13 +31,15 @@ function AddPersonalBankAccountPage() { const [personalBankAccount] = useOnyx(ONYXKEYS.PERSONAL_BANK_ACCOUNT); const shouldShowSuccess = personalBankAccount?.shouldShowSuccess ?? false; const topmostFullScreenRoute = navigationRef.current?.getRootState()?.routes.findLast((route) => isFullScreenName(route.name)); + const activeTab = getActiveTabName(topmostFullScreenRoute); const kycWallRef = useContext(KYCWallContext); const goBack = () => { - switch (topmostFullScreenRoute?.name) { + switch (activeTab) { case NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR: Navigation.goBack(ROUTES.SETTINGS_WALLET); break; + case SCREENS.HOME: case NAVIGATORS.REPORTS_SPLIT_NAVIGATOR: Navigation.closeRHPFlow(); break; From a63cee5d2fbcf1e5eedd89d3085e62a510b12b9e Mon Sep 17 00:00:00 2001 From: Sergei Sharabai <105950333+sharabai@users.noreply.github.com> Date: Mon, 27 Jul 2026 14:40:41 +0200 Subject: [PATCH 6/8] Apply parrot diff --- src/languages/de.ts | 4 +--- src/languages/es.ts | 4 +--- src/languages/fr.ts | 4 +--- src/languages/it.ts | 4 +--- src/languages/ja.ts | 4 +--- src/languages/nl.ts | 4 +--- src/languages/pl.ts | 4 +--- src/languages/pt-BR.ts | 4 +--- src/languages/zh-hans.ts | 4 +--- 9 files changed, 9 insertions(+), 27 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index cd7a0ad41325..f6cb6092ebff 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -964,9 +964,7 @@ const translations: TranslationDeepObject = { title: 'Zeitkritisch', addShippingAddress: {title: 'Wir benötigen deine Versandadresse', subtitle: 'Geben Sie eine Adresse an, um Ihre Expensify Karte zu erhalten.', cta: 'Adresse hinzufügen'}, addPaymentCard: {title: 'Fügen Sie eine Zahlungskarte hinzu, um Expensify weiter zu nutzen', subtitle: 'Konto > Abonnement', cta: 'Hinzufügen'}, - addBankAccount: { - title: 'Fügen Sie ein Bankkonto hinzu, um Rückerstattungen zu erhalten', - }, + addBankAccount: {title: 'Fügen Sie ein Bankkonto hinzu, um eine Erstattung zu erhalten'}, activateCard: {title: 'Aktivieren Sie Ihre Expensify Karte', subtitle: 'Validieren Sie Ihre Karte und beginnen Sie mit dem Ausgeben.', cta: 'Aktivieren'}, reviewCardFraud: { title: 'Möglichen Betrug mit Ihrer Expensify Karte überprüfen', diff --git a/src/languages/es.ts b/src/languages/es.ts index 1b0bc7478786..8c29a3f5ad1a 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -929,9 +929,7 @@ const translations: TranslationDeepObject = { subtitle: 'Cuenta > Suscripción', cta: 'Añadir', }, - addBankAccount: { - title: 'Añade una cuenta bancaria para recibir tus reembolsos', - }, + addBankAccount: {title: 'Añade una cuenta bancaria para recibir el reembolso'}, activateCard: { title: 'Activa tu Tarjeta Expensify', subtitle: 'Valida tu tarjeta y empieza a gastar.', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 50252f59ad75..16bdc7fd84b7 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -967,9 +967,7 @@ const translations: TranslationDeepObject = { title: 'Urgent', addShippingAddress: {title: 'Nous avons besoin de votre adresse de livraison', subtitle: 'Indiquez une adresse pour recevoir votre Carte Expensify.', cta: 'Ajouter une adresse'}, addPaymentCard: {title: 'Ajoutez une carte de paiement pour continuer à utiliser Expensify', subtitle: 'Compte > Abonnement', cta: 'Ajouter'}, - addBankAccount: { - title: 'Ajoutez un compte bancaire pour recevoir vos remboursements', - }, + addBankAccount: {title: 'Ajoutez un compte bancaire pour être remboursé'}, activateCard: {title: 'Activer votre Carte Expensify', subtitle: 'Validez votre carte et commencez à dépenser.', cta: 'Activer'}, reviewCardFraud: { title: 'Examiner une éventuelle fraude sur votre Carte Expensify', diff --git a/src/languages/it.ts b/src/languages/it.ts index a97e1ee68516..63a2e9165bc8 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -965,9 +965,7 @@ const translations: TranslationDeepObject = { title: 'Sensibile al tempo', addShippingAddress: {title: 'Ci serve il tuo indirizzo di spedizione', subtitle: 'Fornisci un indirizzo per ricevere la tua Carta Expensify.', cta: 'Aggiungi indirizzo'}, addPaymentCard: {title: 'Aggiungi una carta di pagamento per continuare a usare Expensify', subtitle: 'Account > Abbonamento', cta: 'Aggiungi'}, - addBankAccount: { - title: 'Aggiungi un conto bancario per ricevere i rimborsi', - }, + addBankAccount: {title: 'Aggiungi un conto bancario per ricevere il rimborso'}, activateCard: {title: 'Attiva la tua Carta Expensify', subtitle: 'Convalida la tua carta e inizia a spendere.', cta: 'Attiva'}, reviewCardFraud: { title: 'Esamina una possibile frode sulla tua Carta Expensify', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 2543093b7dea..aa94a25afb6b 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -954,9 +954,7 @@ const translations: TranslationDeepObject = { title: '時間に敏感', addShippingAddress: {title: '配送先住所が必要です', subtitle: 'Expensify カードを受け取る住所を入力してください。', cta: '住所を追加'}, addPaymentCard: {title: 'Expensify を引き続きご利用いただくには、支払いカードを追加してください', subtitle: 'アカウント > サブスクリプション', cta: '追加'}, - addBankAccount: { - title: '払い戻しを受け取るために銀行口座を追加してください', - }, + addBankAccount: {title: '銀行口座を追加して払い戻しを受け取りましょう'}, activateCard: {title: 'Expensify カードを有効化する', subtitle: 'カードを認証して支出を始めましょう。', cta: '有効化'}, reviewCardFraud: { title: 'Expensify カードの不正利用の可能性を確認する', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 804a5f077293..b4c2e9b14ff5 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -963,9 +963,7 @@ const translations: TranslationDeepObject = { title: 'Tijdgevoelig', addShippingAddress: {title: 'We hebben je verzendadres nodig', subtitle: 'Geef een adres op om je Expensify Kaart te ontvangen.', cta: 'Adres toevoegen'}, addPaymentCard: {title: 'Voeg een betaalkaart toe om Expensify te blijven gebruiken', subtitle: 'Account > Abonnement', cta: 'Toevoegen'}, - addBankAccount: { - title: 'Voeg een bankrekening toe om vergoedingen te ontvangen', - }, + addBankAccount: {title: 'Voeg een bankrekening toe om je terugbetaling te ontvangen'}, activateCard: {title: 'Activeer je Expensify Kaart', subtitle: 'Valideer je kaart en begin met uitgeven.', cta: 'Activeren'}, reviewCardFraud: { title: 'Controleer mogelijk misbruik van je Expensify Kaart', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 1123d54abb8d..dd237b533642 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -965,9 +965,7 @@ const translations: TranslationDeepObject = { title: 'Wymaga szybkiej reakcji', addShippingAddress: {title: 'Potrzebujemy Twojego adresu wysyłki', subtitle: 'Podaj adres, na który mamy wysłać twoją Kartę Expensify.', cta: 'Dodaj adres'}, addPaymentCard: {title: 'Dodaj kartę płatniczą, żeby dalej korzystać z Expensify', subtitle: 'Konto > Subskrypcja', cta: 'Dodaj'}, - addBankAccount: { - title: 'Dodaj konto bankowe, aby otrzymywać zwroty', - }, + addBankAccount: {title: 'Dodaj konto bankowe, aby otrzymać zwrot'}, activateCard: {title: 'Aktywuj swoją Kartę Expensify', subtitle: 'Zatwierdź swoją kartę i zacznij wydawać.', cta: 'Aktywuj'}, reviewCardFraud: { title: 'Sprawdź potencjalne oszustwo na swojej Karcie Expensify', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 815e16930eb1..f170d55a27fd 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -963,9 +963,7 @@ const translations: TranslationDeepObject = { title: 'Urgente', addShippingAddress: {title: 'Precisamos do seu endereço de entrega', subtitle: 'Informe um endereço para receber seu Cartão Expensify.', cta: 'Adicionar endereço'}, addPaymentCard: {title: 'Adicione um cartão de pagamento para continuar usando o Expensify', subtitle: 'Conta > Assinatura', cta: 'Adicionar'}, - addBankAccount: { - title: 'Adicione uma conta bancária para receber reembolsos', - }, + addBankAccount: {title: 'Adicione uma conta bancária para ser reembolsado'}, activateCard: {title: 'Ative seu Cartão Expensify', subtitle: 'Valide seu cartão e comece a gastar.', cta: 'Ativar'}, reviewCardFraud: { title: 'Analisar possível fraude no seu Cartão Expensify', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 06ebf221ab85..04bf2d21e13e 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -937,9 +937,7 @@ const translations: TranslationDeepObject = { title: '时间敏感', addShippingAddress: {title: '我们需要您的收货地址', subtitle: '请提供一个地址以接收您的 Expensify 卡。', cta: '添加地址'}, addPaymentCard: {title: '添加支付卡以继续使用 Expensify', subtitle: '账户 > 订阅', cta: '添加'}, - addBankAccount: { - title: '添加银行账户以便接收报销款', - }, + addBankAccount: {title: '添加银行账户以接收报销'}, activateCard: {title: '激活你的 Expensify 卡', subtitle: '验证您的银行卡并开始消费。', cta: '启用'}, reviewCardFraud: { title: '审查您 Expensify 卡上的潜在欺诈交易', From fe3b338361864493b94b477504b63e086128a856 Mon Sep 17 00:00:00 2001 From: Sergei Sharabai <105950333+sharabai@users.noreply.github.com> Date: Wed, 29 Jul 2026 19:59:56 +0200 Subject: [PATCH 7/8] Address comments --- .../Expensify-Home-Overview.md | 1 + src/CONST/index.ts | 6 + src/libs/ReportUtils.ts | 13 +- .../hooks/useTimeSensitiveAddBankAccount.ts | 4 +- .../items/AddBankAccount.tsx | 9 +- .../ReimbursementQueuedContent.tsx | 4 +- tests/unit/ReportUtilsTest.ts | 285 +++++++----------- .../pages/AddPersonalBankAccountPageTest.tsx | 89 ++++++ 8 files changed, 223 insertions(+), 188 deletions(-) create mode 100644 tests/unit/pages/AddPersonalBankAccountPageTest.tsx diff --git a/docs/articles/new-expensify/getting-started/Expensify-Home-Overview.md b/docs/articles/new-expensify/getting-started/Expensify-Home-Overview.md index 8b2c437734a3..36dcca7df4fe 100644 --- a/docs/articles/new-expensify/getting-started/Expensify-Home-Overview.md +++ b/docs/articles/new-expensify/getting-started/Expensify-Home-Overview.md @@ -50,6 +50,7 @@ The **Time-sensitive alerts** section appears only when there is something that These alerts appear when: - A workflow is blocked, such as a broken bank or accounting connection due to expired or invalid credentials - A bank account is locked, such as a business or personal bank account that has been locked due to a failed debit or bank-side restriction +- A reimbursement is waiting for the payee to add a personal deposit account - There is potential risk, such as suspected Expensify Card fraud on an active card - An action must be taken within a short window, such as a limited-time offer or early adoption discount diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 21102c4e1fc6..4e8d08b02e03 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -3769,6 +3769,12 @@ const CONST = { BANK_ACCOUNT: 'bankAccountID', }, + /** The payment method a payee still has to set up before a queued payment can settle */ + MISSING_PAYMENT_METHODS: { + BANK_ACCOUNT: 'bankAccount', + WALLET: 'wallet', + }, + IOU: { MAX_RECENT_REPORTS_TO_SHOW: 5, MAX_RECENT_ATTENDEES: 40, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 7fe730435c8a..9d55d51f8c7a 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -214,7 +214,6 @@ import { isMovedAction, isOlderReportAction, isPendingRemove, - isReimbursementQueuedAction, isReopenedAction, isReportActionVisible, isReportPreviewAction, @@ -921,7 +920,7 @@ type Ancestor = { shouldDisplayNewMarker: boolean; }; -type MissingPaymentMethod = 'bankAccount' | 'wallet'; +type MissingPaymentMethod = ValueOf; type OutstandingChildRequest = { hasOutstandingChildRequest?: boolean; @@ -11438,10 +11437,10 @@ function getMissingPaymentMethodForQueuedPayment( ): MissingPaymentMethod | undefined { const paymentType = getOriginalMessage(reportAction)?.paymentType; if (paymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY) { - return !userWalletTierName || userWalletTierName === CONST.WALLET.TIER_NAME.SILVER ? 'wallet' : undefined; + return !userWalletTierName || userWalletTierName === CONST.WALLET.TIER_NAME.SILVER ? CONST.MISSING_PAYMENT_METHODS.WALLET : undefined; } - return !hasCreditBankAccount(bankAccountList) ? 'bankAccount' : undefined; + return !hasCreditBankAccount(bankAccountList) ? CONST.MISSING_PAYMENT_METHODS.BANK_ACCOUNT : undefined; } /** @@ -11450,11 +11449,11 @@ function getMissingPaymentMethodForQueuedPayment( function getIndicatedMissingPaymentMethod( userWalletTierName: string | undefined, reportId: string | undefined, - reportAction: ReportAction, + reportAction: ReportAction, bankAccountList: OnyxEntry, ): MissingPaymentMethod | undefined { - const isSubmitterOfUnsettledReport = reportId && isCurrentUserSubmitter(getReport(reportId, deprecatedAllReports)) && !isSettled(reportId); - if (!reportId || !isSubmitterOfUnsettledReport || !isReimbursementQueuedAction(reportAction)) { + const isSubmitterOfUnsettledReport = !!reportId && isCurrentUserSubmitter(getReport(reportId, deprecatedAllReports)) && !isSettled(reportId); + if (!isSubmitterOfUnsettledReport) { return undefined; } diff --git a/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveAddBankAccount.ts b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveAddBankAccount.ts index c90576af12a0..49953e3a09d4 100644 --- a/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveAddBankAccount.ts +++ b/src/pages/home/TimeSensitiveSection/hooks/useTimeSensitiveAddBankAccount.ts @@ -7,7 +7,7 @@ import hasCreditBankAccount from '@libs/actions/ReimbursementAccount/hasCreditBa import {isNewerReportAction, isReimbursementQueuedAction} from '@libs/ReportActionsUtils'; import {getMissingPaymentMethodForQueuedPayment} from '@libs/ReportUtils'; -import type CONST from '@src/CONST'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Report, ReportAction, ReportActions} from '@src/types/onyx'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; @@ -50,7 +50,7 @@ function useTimeSensitiveAddBankAccount() { return (waitingReportIDs ?? []).some((reportID) => { const queuedAction = getLatestReimbursementQueuedAction(reportID, allReportActions); - return !!queuedAction && getMissingPaymentMethodForQueuedPayment(userWalletTierName, queuedAction, bankAccountList) === 'bankAccount'; + return !!queuedAction && getMissingPaymentMethodForQueuedPayment(userWalletTierName, queuedAction, bankAccountList) === CONST.MISSING_PAYMENT_METHODS.BANK_ACCOUNT; }); }; const [shouldShowAddBankAccount = false] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS, {selector: shouldShowAddBankAccountSelector}); diff --git a/src/pages/home/TimeSensitiveSection/items/AddBankAccount.tsx b/src/pages/home/TimeSensitiveSection/items/AddBankAccount.tsx index 218e4d1c4034..0db3dc0226bf 100644 --- a/src/pages/home/TimeSensitiveSection/items/AddBankAccount.tsx +++ b/src/pages/home/TimeSensitiveSection/items/AddBankAccount.tsx @@ -20,18 +20,15 @@ function AddBankAccount() { const {translate} = useLocalize(); const icons = useMemoizedLazyExpensifyIcons(['Bank']); const [isUserValidated] = useOnyx(ONYXKEYS.ACCOUNT, {selector: isUserValidatedSelector}); - const title = translate('homePage.timeSensitiveSection.addBankAccount.title'); - const subtitle = translate('common.wallet'); - const ctaText = translate('common.add'); return ( openPersonalBankAccountSetupView({isUserValidated})} buttonProps={{success: true}} /> diff --git a/src/pages/inbox/report/actionContents/ReimbursementQueuedContent.tsx b/src/pages/inbox/report/actionContents/ReimbursementQueuedContent.tsx index 35c9eb640969..b634cc028089 100644 --- a/src/pages/inbox/report/actionContents/ReimbursementQueuedContent.tsx +++ b/src/pages/inbox/report/actionContents/ReimbursementQueuedContent.tsx @@ -60,7 +60,7 @@ function ReimbursementQueuedContent({action, report, iouReport}: ReimbursementQu return ( <> - {missingPaymentMethod === 'bankAccount' && ( + {missingPaymentMethod === CONST.MISSING_PAYMENT_METHODS.BANK_ACCOUNT && (