Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 2 additions & 5 deletions src/hooks/useSearchTypeMenuSections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -54,9 +53,7 @@ type UseSearchTypeMenuSectionsParams = {
*/
const useSearchTypeMenuSections = (queryParams?: UseSearchTypeMenuSectionsParams) => {
const {hash, similarSearchHash} = queryParams ?? {};
const {translate} = useLocalize();
const cardSelector = useCallback((allCards: OnyxEntry<NonPersonalAndWorkspaceCardListDerivedValue>) => 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();

Expand Down
50 changes: 32 additions & 18 deletions src/libs/CardFeedUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's add tests for this util

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.
*
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -618,6 +631,7 @@ export {
generateDomainFeedData,
getDomainFeedData,
getCardFeedsForDisplay,
getExpensifyCardFeedsForDisplay,
getCardFeedsForDisplayPerPolicy,
getCombinedCardFeedsFromAllFeeds,
getCardFeedStatus,
Expand Down
12 changes: 7 additions & 5 deletions src/selectors/Card.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -68,9 +67,12 @@ const getBankLinkedPersonalCards = (cards: OnyxEntry<CardList>): CardList => {
/**
* Selects the Expensify Card feed from the card list and returns the first one.
*/
const defaultExpensifyCardSelector = (allCards: OnyxEntry<NonPersonalAndWorkspaceCardListDerivedValue>, translate: LocalizedTranslate) => {
const cards = getCardFeedsForDisplay({}, allCards, translate);
return Object.values(cards)?.at(0);
const defaultExpensifyCardSelector = (allCards: OnyxEntry<NonPersonalAndWorkspaceCardListDerivedValue>) => {
const cards = getExpensifyCardFeedsForDisplay(allCards ?? undefined);
for (const card of Object.values(cards)) {
return card;
}
return undefined;
Comment on lines +72 to +75
Copy link
Contributor

@ZhenjaHorbach ZhenjaHorbach Mar 10, 2026

Choose a reason for hiding this comment

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

Do we really need for here?

Can we just use return Object.values(cards)?.at(0);?

};

/**
Expand Down
95 changes: 94 additions & 1 deletion tests/unit/CardFeedUtilsTest.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);
});
});
15 changes: 7 additions & 8 deletions tests/unit/selectors/CardTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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', () => {
Expand All @@ -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', () => {
Expand All @@ -179,15 +178,15 @@ 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', () => {
const allCards: CardList = {
'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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down