Skip to content

Commit 64e70b6

Browse files
Add third-party cards to YourSpend section
Merges employer-feed and personal Plaid card rows into the YourSpend hook alongside Expensify cards, tagged with a `YOUR_SPEND_CARD_KIND` discriminator so `CardRow` can pick the right artwork branch (bank icon via `feed|domainID` or bare Plaid feed). Adds a new `getDisplayableThirdPartyCards` selector that filters out broken-feed and broken-connection cards.
1 parent 8c53336 commit 64e70b6

9 files changed

Lines changed: 758 additions & 67 deletions

File tree

src/components/CardFeedIcon.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,18 @@ import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset';
44
import useThemeIllustrations from '@hooks/useThemeIllustrations';
55
import {getCardFeedIcon, getPlaidInstitutionIconUrl, getPlaidInstitutionId} from '@libs/CardUtils';
66
import type {CardFeedWithDomainID} from '@src/types/onyx';
7+
import type {CardFeedWithNumber} from '@src/types/onyx/CardFeeds';
78
import type {IconProps} from './Icon';
89
import Icon from './Icon';
910
import PlaidCardFeedIcon from './PlaidCardFeedIcon';
1011

1112
type CardFeedIconProps = {
1213
isExpensifyCardFeed?: boolean;
13-
selectedFeed?: CardFeedWithDomainID | undefined;
14+
// Accepts either `CardFeedWithDomainID` (employer feeds, e.g. `vcf|123`) or the
15+
// bare `CardFeedWithNumber` (personal Plaid cards, e.g. `plaid.ins_109508`).
16+
// The internal `getPlaidInstitutionId` / `getCardFeedIcon` helpers already accept
17+
// the wider type.
18+
selectedFeed?: CardFeedWithDomainID | CardFeedWithNumber | undefined;
1419
iconProps?: Partial<IconProps>;
1520
useSkeletonLoader?: boolean;
1621
};

src/libs/CardUtils.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import type {
4141
CompanyFeeds,
4242
NonConnectableBankName,
4343
} from '@src/types/onyx/CardFeeds';
44+
import type {CardFeedErrors} from '@src/types/onyx/DerivedValues';
4445
import type {SelectedTimezone} from '@src/types/onyx/PersonalDetails';
4546
import type {Connections} from '@src/types/onyx/Policy';
4647
import {isEmptyObject} from '@src/types/utils/EmptyObject';
@@ -1698,6 +1699,39 @@ function getDisplayableExpensifyCards(cardList: CardList | undefined): Card[] {
16981699
});
16991700
}
17001701

1702+
/**
1703+
* Active, non-Expensify, non-cash cards (employer feed or personal Plaid) that are not flagged
1704+
* as broken at the card- or feed-level, sorted by cardID ascending.
1705+
*
1706+
* No `domainName` dedupe: third-party cards don't share the Expensify "one domain ⇒ one
1707+
* physical+virtual pair" invariant, so deduping would silently collapse distinct cards.
1708+
*
1709+
* `cardFeedErrors` maps are `Record<string, Card>` (presence = broken), so filter with
1710+
* truthy/falsy — `=== true` would always be false and let broken cards through.
1711+
*/
1712+
function getDisplayableThirdPartyCards(cardList: CardList | undefined, cardFeedErrors: Pick<CardFeedErrors, 'cardsWithBrokenFeedConnection' | 'personalCardsWithBrokenConnection'>): Card[] {
1713+
if (!cardList) {
1714+
return [];
1715+
}
1716+
1717+
const {cardsWithBrokenFeedConnection, personalCardsWithBrokenConnection} = cardFeedErrors;
1718+
const cards = Object.values(cardList).filter(
1719+
(card) =>
1720+
CONST.EXPENSIFY_CARD.ACTIVE_STATES.includes(card.state ?? 0) &&
1721+
!isExpensifyCard(card) &&
1722+
(!!card.domainName || isPersonalCard(card)) &&
1723+
card.cardName !== CONST.COMPANY_CARDS.CARD_NAME.CASH &&
1724+
!isCardConnectionBroken(card) &&
1725+
!cardsWithBrokenFeedConnection[card.cardID] &&
1726+
!personalCardsWithBrokenConnection[card.cardID],
1727+
);
1728+
1729+
// Stable sort by `getAssignedCardSortKey` (constant `2` for all non-Expensify cards),
1730+
// so the result preserves the cardID-ascending order produced by `Object.values` over
1731+
// integer-indexed keys.
1732+
return lodashSortBy(cards, getAssignedCardSortKey);
1733+
}
1734+
17011735
function getCardCurrency(card?: OnyxEntry<Card>, cardSettings?: OnyxEntry<ExpensifyCardSettings>): string {
17021736
// If currency is set on the card itself, use it.
17031737
if (card?.nameValuePairs?.currency) {
@@ -1912,6 +1946,7 @@ export {
19121946
isCardInactive,
19131947
isCardWithPotentialFraud,
19141948
getDisplayableExpensifyCards,
1949+
getDisplayableThirdPartyCards,
19151950
isExpiredCard,
19161951
getCardCurrency,
19171952
getSelectedCardsSharedCurrency,

src/pages/home/YourSpendSection/CardRow.tsx

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
99
import useLocalize from '@hooks/useLocalize';
1010
import useTheme from '@hooks/useTheme';
1111
import useThemeStyles from '@hooks/useThemeStyles';
12+
import {getCardFeedWithDomainID} from '@libs/CardUtils';
1213
import {convertToDisplayString} from '@libs/CurrencyUtils';
1314
import Navigation from '@libs/Navigation/Navigation';
1415
import variables from '@styles/variables';
1516
import ROUTES from '@src/ROUTES';
1617
import RemainingLimitCircle from './RemainingLimitCircle';
1718
import type {useYourSpendData} from './useYourSpendData';
19+
import {YOUR_SPEND_CARD_KIND} from './useYourSpendData';
1820

1921
type CardRowProps = {
2022
cardRow: ReturnType<typeof useYourSpendData>['cardRows'][number];
@@ -31,6 +33,41 @@ function CardRow({cardRow, wrapperStyle}: CardRowProps) {
3133

3234
const cardTotal = cardRow.total !== undefined ? convertToDisplayString(cardRow.total, cardRow.currency) : undefined;
3335

36+
// Pick the artwork branch up front so the JSX below stays readable.
37+
// - Expensify Card: keep the existing illustrated feed icon.
38+
// - Third-party with `fundID`: employer-feed company card → `feed|domainID` key.
39+
// - Third-party without `fundID`: personal Plaid card → pass the bare `bank`
40+
// (`plaid.ins_…`) directly. `CardFeedIcon` resolves the Plaid institution icon
41+
// internally via `getPlaidInstitutionId(selectedFeed)`.
42+
const iconProps = {
43+
width: variables.cardIconWidth,
44+
height: variables.cardIconHeight,
45+
additionalStyles: [styles.overflowHidden, styles.br1],
46+
};
47+
let leftIcon: React.ReactElement;
48+
if (cardRow.kind === YOUR_SPEND_CARD_KIND.EXPENSIFY) {
49+
leftIcon = (
50+
<CardFeedIcon
51+
isExpensifyCardFeed
52+
iconProps={iconProps}
53+
/>
54+
);
55+
} else if (cardRow.fundID !== undefined) {
56+
leftIcon = (
57+
<CardFeedIcon
58+
selectedFeed={getCardFeedWithDomainID(cardRow.bank, cardRow.fundID)}
59+
iconProps={iconProps}
60+
/>
61+
);
62+
} else {
63+
leftIcon = (
64+
<CardFeedIcon
65+
selectedFeed={cardRow.bank}
66+
iconProps={iconProps}
67+
/>
68+
);
69+
}
70+
3471
return (
3572
<View
3673
testID={`your-spend-card-row-${cardRow.cardID}`}
@@ -56,18 +93,7 @@ function CardRow({cardRow, wrapperStyle}: CardRowProps) {
5693
</View>
5794
</View>
5895
}
59-
leftComponent={
60-
<View style={[styles.justifyContentCenter, styles.h10]}>
61-
<CardFeedIcon
62-
isExpensifyCardFeed
63-
iconProps={{
64-
width: variables.cardIconWidth,
65-
height: variables.cardIconHeight,
66-
additionalStyles: [styles.overflowHidden, styles.br1],
67-
}}
68-
/>
69-
</View>
70-
}
96+
leftComponent={<View style={[styles.justifyContentCenter, styles.h10]}>{leftIcon}</View>}
7197
wrapperStyle={wrapperStyle}
7298
hasSubMenuItems
7399
shouldCheckActionAllowedOnPress={false}

src/pages/home/YourSpendSection/SpendSummaryRow.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
1111
import {convertToDisplayString} from '@libs/CurrencyUtils';
1212
import variables from '@styles/variables';
1313
import type IconAsset from '@src/types/utils/IconAsset';
14-
import YOUR_SPEND_ROW_STATE from './const';
14+
import {YOUR_SPEND_ROW_STATE} from './const';
1515
import type {useYourSpendData} from './useYourSpendData';
1616

1717
// Skeleton geometry mirrors `ForYouSection/ForYouSkeleton.tsx` so the home page

src/pages/home/YourSpendSection/const.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,14 @@ const YOUR_SPEND_ROW_STATE = {
55
HIDDEN_EMPTY: 'hiddenEmpty',
66
} as const;
77

8-
export default YOUR_SPEND_ROW_STATE;
8+
/**
9+
* Discriminator for a card row's artwork branch:
10+
* - `EXPENSIFY` keeps the existing Expensify Card icon.
11+
* - `THIRD_PARTY` switches to bank artwork (employer feed) or a Plaid institution icon.
12+
*/
13+
const YOUR_SPEND_CARD_KIND = {
14+
EXPENSIFY: 'expensify',
15+
THIRD_PARTY: 'thirdParty',
16+
} as const;
17+
18+
export {YOUR_SPEND_ROW_STATE, YOUR_SPEND_CARD_KIND};

0 commit comments

Comments
 (0)