diff --git a/src/hooks/useSearchTypeMenuSections.ts b/src/hooks/useSearchTypeMenuSections.ts index 9816f48c9163e..3fd0af18de08f 100644 --- a/src/hooks/useSearchTypeMenuSections.ts +++ b/src/hooks/useSearchTypeMenuSections.ts @@ -4,11 +4,10 @@ import type {OnyxEntry} from 'react-native-onyx'; import {areAllGroupPoliciesExpenseChatDisabled} from '@libs/PolicyUtils'; import {createTypeMenuSections} from '@libs/SearchUIUtils'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {NonPersonalAndWorkspaceCardListDerivedValue, Policy, Session} from '@src/types/onyx'; +import type {Policy, Session} from '@src/types/onyx'; import useCardFeedsForDisplay from './useCardFeedsForDisplay'; import useCreateEmptyReportConfirmation from './useCreateEmptyReportConfirmation'; import {useMemoizedLazyExpensifyIcons} from './useLazyAsset'; -import useLocalize from './useLocalize'; import useMappedPolicies from './useMappedPolicies'; import useNetwork from './useNetwork'; import useOnyx from './useOnyx'; @@ -54,9 +53,7 @@ type UseSearchTypeMenuSectionsParams = { */ const useSearchTypeMenuSections = (queryParams?: UseSearchTypeMenuSectionsParams) => { const {hash, similarSearchHash} = queryParams ?? {}; - const {translate} = useLocalize(); - const cardSelector = useCallback((allCards: OnyxEntry) => defaultExpensifyCardSelector(allCards, translate), [translate]); - const [defaultExpensifyCard] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST, {selector: cardSelector}, [cardSelector]); + const [defaultExpensifyCard] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST, {selector: defaultExpensifyCardSelector}); const {defaultCardFeed, cardFeedsByPolicy} = useCardFeedsForDisplay(); diff --git a/src/libs/CardFeedUtils.ts b/src/libs/CardFeedUtils.ts index b50ec309df409..6531ae91aaf74 100644 --- a/src/libs/CardFeedUtils.ts +++ b/src/libs/CardFeedUtils.ts @@ -435,6 +435,36 @@ const generateSelectedCards = ( return [...new Set([...selectedCards, ...(cards ?? [])])]; }; +/** + * Given a card list, return a map of Expensify Card feeds keyed by "${fundID}_${BANK}". + * This is extracted from getCardFeedsForDisplay so it can be called independently + * (e.g. from selectors that only need Expensify Card feeds). + */ +function getExpensifyCardFeedsForDisplay(allCards: CardList | undefined): CardFeedsForDisplay { + const result = {} as CardFeedsForDisplay; + + for (const card of Object.values(allCards ?? {})) { + if (card.bank !== CONST.EXPENSIFY_CARD.BANK || !card.fundID) { + continue; + } + + const id = `${card.fundID}_${CONST.EXPENSIFY_CARD.BANK}`; + + if (result[id]) { + continue; + } + + result[id] = { + id, + feed: CONST.EXPENSIFY_CARD.BANK, + fundID: card.fundID, + name: CONST.EXPENSIFY_CARD.BANK, + }; + } + + return result; +} + /** * Given a collection of card feeds, return formatted card feeds. * @@ -472,24 +502,7 @@ function getCardFeedsForDisplay( } } - for (const card of Object.values(allCards ?? {})) { - if (card.bank !== CONST.EXPENSIFY_CARD.BANK || !card.fundID) { - continue; - } - - const id = `${card.fundID}_${CONST.EXPENSIFY_CARD.BANK}`; - - if (cardFeedsForDisplay[id]) { - continue; - } - - cardFeedsForDisplay[id] = { - id, - feed: CONST.EXPENSIFY_CARD.BANK, - fundID: card.fundID, - name: CONST.EXPENSIFY_CARD.BANK, - }; - } + Object.assign(cardFeedsForDisplay, getExpensifyCardFeedsForDisplay(allCards)); return cardFeedsForDisplay; } @@ -618,6 +631,7 @@ export { generateDomainFeedData, getDomainFeedData, getCardFeedsForDisplay, + getExpensifyCardFeedsForDisplay, getCardFeedsForDisplayPerPolicy, getCombinedCardFeedsFromAllFeeds, getCardFeedStatus, diff --git a/src/selectors/Card.ts b/src/selectors/Card.ts index 375fbcfc89dad..978d7b1934a42 100644 --- a/src/selectors/Card.ts +++ b/src/selectors/Card.ts @@ -1,6 +1,5 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import type {LocalizedTranslate} from '@components/LocaleContextProvider'; -import {getCardFeedsForDisplay} from '@libs/CardFeedUtils'; +import {getExpensifyCardFeedsForDisplay} from '@libs/CardFeedUtils'; import {isCard, isCardHiddenFromSearch, isExpensifyCard, isPersonalCard} from '@libs/CardUtils'; import {filterObject} from '@libs/ObjectUtils'; import CONST from '@src/CONST'; @@ -68,9 +67,12 @@ const getBankLinkedPersonalCards = (cards: OnyxEntry): CardList => { /** * Selects the Expensify Card feed from the card list and returns the first one. */ -const defaultExpensifyCardSelector = (allCards: OnyxEntry, translate: LocalizedTranslate) => { - const cards = getCardFeedsForDisplay({}, allCards, translate); - return Object.values(cards)?.at(0); +const defaultExpensifyCardSelector = (allCards: OnyxEntry) => { + const cards = getExpensifyCardFeedsForDisplay(allCards ?? undefined); + for (const card of Object.values(cards)) { + return card; + } + return undefined; }; /** diff --git a/tests/unit/CardFeedUtilsTest.ts b/tests/unit/CardFeedUtilsTest.ts index 9974d4eff459d..2cdc5d8c3494c 100644 --- a/tests/unit/CardFeedUtilsTest.ts +++ b/tests/unit/CardFeedUtilsTest.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/naming-convention */ import type {OnyxCollection} from 'react-native-onyx'; -import {getCardFeedNamesWithType, getCardFeedsForDisplay, getCardFeedsForDisplayPerPolicy, getSelectedCardsFromFeeds} from '@libs/CardFeedUtils'; +import {getCardFeedNamesWithType, getCardFeedsForDisplay, getCardFeedsForDisplayPerPolicy, getExpensifyCardFeedsForDisplay, getSelectedCardsFromFeeds} from '@libs/CardFeedUtils'; import CONST from '@src/CONST'; import IntlStore from '@src/languages/IntlStore'; import type {CardFeeds, CardList, CompanyCardFeed, WorkspaceCardsList} from '@src/types/onyx'; @@ -160,3 +160,96 @@ describe('Card Feed Utils', () => { }); }); }); + +describe('getExpensifyCardFeedsForDisplay', () => { + it('returns empty object when allCards is undefined', () => { + expect(getExpensifyCardFeedsForDisplay(undefined)).toEqual({}); + }); + + it('returns empty object when allCards is empty', () => { + expect(getExpensifyCardFeedsForDisplay({})).toEqual({}); + }); + + it('returns empty object when no cards have Expensify Card bank', () => { + const allCards = { + '1': {bank: 'vcf', fundID: '5555'}, + '2': {bank: 'stripe', fundID: '6666'}, + } as unknown as CardList; + + expect(getExpensifyCardFeedsForDisplay(allCards)).toEqual({}); + }); + + it('returns empty object when Expensify Cards have no fundID', () => { + const allCards = { + '1': {bank: CONST.EXPENSIFY_CARD.BANK, fundID: undefined}, + '2': {bank: CONST.EXPENSIFY_CARD.BANK, fundID: ''}, + } as unknown as CardList; + + expect(getExpensifyCardFeedsForDisplay(allCards)).toEqual({}); + }); + + it('returns a single feed entry for one Expensify Card with fundID', () => { + const allCards = { + '1': {bank: CONST.EXPENSIFY_CARD.BANK, fundID: '5555'}, + } as unknown as CardList; + + expect(getExpensifyCardFeedsForDisplay(allCards)).toEqual({ + '5555_Expensify Card': {id: '5555_Expensify Card', fundID: '5555', feed: CONST.EXPENSIFY_CARD.BANK, name: CONST.EXPENSIFY_CARD.BANK}, + }); + }); + + it('deduplicates cards with the same fundID', () => { + const allCards = { + '1': {bank: CONST.EXPENSIFY_CARD.BANK, fundID: '5555'}, + '2': {bank: CONST.EXPENSIFY_CARD.BANK, fundID: '5555'}, + '3': {bank: CONST.EXPENSIFY_CARD.BANK, fundID: '5555'}, + } as unknown as CardList; + + const result = getExpensifyCardFeedsForDisplay(allCards); + expect(Object.keys(result)).toHaveLength(1); + expect(result['5555_Expensify Card']).toEqual({id: '5555_Expensify Card', fundID: '5555', feed: CONST.EXPENSIFY_CARD.BANK, name: CONST.EXPENSIFY_CARD.BANK}); + }); + + it('returns separate entries for different fundIDs', () => { + const allCards = { + '1': {bank: CONST.EXPENSIFY_CARD.BANK, fundID: '5555'}, + '2': {bank: CONST.EXPENSIFY_CARD.BANK, fundID: '6666'}, + } as unknown as CardList; + + const result = getExpensifyCardFeedsForDisplay(allCards); + expect(Object.keys(result)).toHaveLength(2); + expect(result['5555_Expensify Card']).toEqual({id: '5555_Expensify Card', fundID: '5555', feed: CONST.EXPENSIFY_CARD.BANK, name: CONST.EXPENSIFY_CARD.BANK}); + expect(result['6666_Expensify Card']).toEqual({id: '6666_Expensify Card', fundID: '6666', feed: CONST.EXPENSIFY_CARD.BANK, name: CONST.EXPENSIFY_CARD.BANK}); + }); + + it('filters out non-Expensify cards from mixed card list', () => { + const allCards = { + '1': {bank: 'vcf', fundID: '5555'}, + '2': {bank: CONST.EXPENSIFY_CARD.BANK, fundID: '6666'}, + '3': {bank: 'stripe', fundID: '7777'}, + } as unknown as CardList; + + const result = getExpensifyCardFeedsForDisplay(allCards); + expect(Object.keys(result)).toHaveLength(1); + expect(result['6666_Expensify Card']).toBeDefined(); + }); + + it('skips Expensify Cards without fundID while keeping those with fundID', () => { + const allCards = { + '1': {bank: CONST.EXPENSIFY_CARD.BANK, fundID: undefined}, + '2': {bank: CONST.EXPENSIFY_CARD.BANK, fundID: ''}, + '3': {bank: CONST.EXPENSIFY_CARD.BANK, fundID: '8888'}, + } as unknown as CardList; + + const result = getExpensifyCardFeedsForDisplay(allCards); + expect(Object.keys(result)).toHaveLength(1); + expect(result['8888_Expensify Card']).toBeDefined(); + }); + + it('produces the same Expensify Card entries as getCardFeedsForDisplay', () => { + const result = getExpensifyCardFeedsForDisplay(cardListMock); + const fullResult = getCardFeedsForDisplay({}, cardListMock, translateLocal); + + expect(result).toEqual(fullResult); + }); +}); diff --git a/tests/unit/selectors/CardTest.ts b/tests/unit/selectors/CardTest.ts index 21edb415093b4..5c6d553d5c689 100644 --- a/tests/unit/selectors/CardTest.ts +++ b/tests/unit/selectors/CardTest.ts @@ -6,7 +6,6 @@ import {isCard, isCardPendingActivate, isCardPendingIssue, isCardWithPotentialFr import CONST from '@src/CONST'; import type {Card, CardList} from '@src/types/onyx'; import createRandomCard, {createRandomCompanyCard, createRandomExpensifyCard} from '../../utils/collections/card'; -import {translateLocal} from '../../utils/TestHelper'; /** * Test helper replicating the logic that was moved inline into useTimeSensitiveCards hook. @@ -160,8 +159,8 @@ describe('filterCardsHiddenFromSearch', () => { describe('defaultExpensifyCardSelector', () => { it('Should return undefined if allCards is undefined or empty', () => { - expect(defaultExpensifyCardSelector(undefined, translateLocal)).toBeUndefined(); - expect(defaultExpensifyCardSelector({}, translateLocal)).toBeUndefined(); + expect(defaultExpensifyCardSelector(undefined)).toBeUndefined(); + expect(defaultExpensifyCardSelector({})).toBeUndefined(); }); it('Should return undefined if cards do not have Expensify Card bank', () => { @@ -170,7 +169,7 @@ describe('defaultExpensifyCardSelector', () => { '2': createRandomCompanyCard(2, {bank: 'stripe'}), }; - expect(defaultExpensifyCardSelector(allCards, translateLocal)).toBeUndefined(); + expect(defaultExpensifyCardSelector(allCards)).toBeUndefined(); }); it('Should return undefined if Expensify Card does not have fundID', () => { @@ -179,7 +178,7 @@ describe('defaultExpensifyCardSelector', () => { '2': createRandomExpensifyCard(2, {fundID: ''}), }; - expect(defaultExpensifyCardSelector(allCards, translateLocal)).toBeUndefined(); + expect(defaultExpensifyCardSelector(allCards)).toBeUndefined(); }); it('Should return the first Expensify Card feed when multiple Expensify Cards exist', () => { @@ -187,7 +186,7 @@ describe('defaultExpensifyCardSelector', () => { '1': createRandomExpensifyCard(1, {fundID: '5555'}), '2': createRandomExpensifyCard(2, {fundID: '6666'}), }; - const result = defaultExpensifyCardSelector(allCards, translateLocal); + const result = defaultExpensifyCardSelector(allCards); expect(result).toEqual({ id: '5555_Expensify Card', feed: CONST.EXPENSIFY_CARD.BANK, @@ -203,7 +202,7 @@ describe('defaultExpensifyCardSelector', () => { '3': createRandomExpensifyCard(3, {fundID: '6666'}), }; - const result = defaultExpensifyCardSelector(allCards, translateLocal); + const result = defaultExpensifyCardSelector(allCards); expect(result).toEqual({ id: '5555_Expensify Card', feed: CONST.EXPENSIFY_CARD.BANK, @@ -217,7 +216,7 @@ describe('defaultExpensifyCardSelector', () => { '1': createRandomExpensifyCard(1, {fundID: undefined}), '2': createRandomExpensifyCard(2, {fundID: '5555'}), }; - const result = defaultExpensifyCardSelector(allCards, translateLocal); + const result = defaultExpensifyCardSelector(allCards); expect(result).toEqual({ id: '5555_Expensify Card', feed: CONST.EXPENSIFY_CARD.BANK,