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
3 changes: 2 additions & 1 deletion src/components/CardFeedIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset';
import useThemeIllustrations from '@hooks/useThemeIllustrations';
import {getCardFeedIcon, getPlaidInstitutionIconUrl, getPlaidInstitutionId} from '@libs/CardUtils';
import type {CardFeedWithDomainID} from '@src/types/onyx';
import type {CardFeedWithNumber} from '@src/types/onyx/CardFeeds';
import type {IconProps} from './Icon';
import Icon from './Icon';
import PlaidCardFeedIcon from './PlaidCardFeedIcon';

type CardFeedIconProps = {
isExpensifyCardFeed?: boolean;
selectedFeed?: CardFeedWithDomainID | undefined;
selectedFeed?: CardFeedWithDomainID | CardFeedWithNumber | undefined;
iconProps?: Partial<IconProps>;
useSkeletonLoader?: boolean;
};
Expand Down
35 changes: 35 additions & 0 deletions src/libs/CardUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import type {
CompanyFeeds,
NonConnectableBankName,
} from '@src/types/onyx/CardFeeds';
import type {CardFeedErrors} from '@src/types/onyx/DerivedValues';
import type {SelectedTimezone} from '@src/types/onyx/PersonalDetails';
import type {Connections} from '@src/types/onyx/Policy';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
Expand Down Expand Up @@ -1698,6 +1699,39 @@ function getDisplayableExpensifyCards(cardList: CardList | undefined): Card[] {
});
}

/**
* Active, non-Expensify, non-cash cards (employer feed or personal Plaid) that are not flagged
* as broken at the card- or feed-level, sorted by cardID ascending.
*
* No `domainName` dedupe: third-party cards don't share the Expensify "one domain ⇒ one
* physical+virtual pair" invariant, so deduping would silently collapse distinct cards.
*
* `cardFeedErrors` maps are `Record<string, Card>` (presence = broken), so filter with
* truthy/falsy — `=== true` would always be false and let broken cards through.
*/
function getDisplayableThirdPartyCards(cardList: CardList | undefined, cardFeedErrors: Pick<CardFeedErrors, 'cardsWithBrokenFeedConnection' | 'personalCardsWithBrokenConnection'>): Card[] {
if (!cardList) {
return [];
}

const {cardsWithBrokenFeedConnection, personalCardsWithBrokenConnection} = cardFeedErrors;
const cards = Object.values(cardList).filter(
(card) =>
CONST.EXPENSIFY_CARD.ACTIVE_STATES.includes(card.state ?? 0) &&
!isExpensifyCard(card) &&
(!!card.domainName || isPersonalCard(card)) &&
card.cardName !== CONST.COMPANY_CARDS.CARD_NAME.CASH &&
!isCardConnectionBroken(card) &&
!cardsWithBrokenFeedConnection[card.cardID] &&
!personalCardsWithBrokenConnection[card.cardID],
);

// Stable sort by `getAssignedCardSortKey` (constant `2` for all non-Expensify cards),
// so the result preserves the cardID-ascending order produced by `Object.values` over
// integer-indexed keys.
return lodashSortBy(cards, getAssignedCardSortKey);
}

function getCardCurrency(card?: OnyxEntry<Card>, cardSettings?: OnyxEntry<ExpensifyCardSettings>): string {
// If currency is set on the card itself, use it.
if (card?.nameValuePairs?.currency) {
Expand Down Expand Up @@ -1912,6 +1946,7 @@ export {
isCardInactive,
isCardWithPotentialFraud,
getDisplayableExpensifyCards,
getDisplayableThirdPartyCards,
isExpiredCard,
getCardCurrency,
getSelectedCardsSharedCurrency,
Expand Down
50 changes: 38 additions & 12 deletions src/pages/home/YourSpendSection/CardRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import {getCardFeedWithDomainID} from '@libs/CardUtils';
import {convertToDisplayString} from '@libs/CurrencyUtils';
import Navigation from '@libs/Navigation/Navigation';
import variables from '@styles/variables';
import ROUTES from '@src/ROUTES';
import RemainingLimitCircle from './RemainingLimitCircle';
import type {useYourSpendData} from './useYourSpendData';
import {YOUR_SPEND_CARD_KIND} from './useYourSpendData';

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

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

// Pick the artwork branch up front so the JSX below stays readable.
// - Expensify Card: keep the existing illustrated feed icon.
// - Third-party with `fundID`: employer-feed company card → `feed|domainID` key.
// - Third-party without `fundID`: personal Plaid card → pass the bare `bank`
// (`plaid.ins_…`) directly. `CardFeedIcon` resolves the Plaid institution icon
// internally via `getPlaidInstitutionId(selectedFeed)`.
const iconProps = {
width: variables.cardIconWidth,
height: variables.cardIconHeight,
additionalStyles: [styles.overflowHidden, styles.br1],
};
let leftIcon: React.ReactElement;
if (cardRow.kind === YOUR_SPEND_CARD_KIND.EXPENSIFY) {
leftIcon = (
<CardFeedIcon
isExpensifyCardFeed
iconProps={iconProps}
/>
);
} else if (cardRow.fundID !== undefined) {
leftIcon = (
<CardFeedIcon
selectedFeed={getCardFeedWithDomainID(cardRow.bank, cardRow.fundID)}
iconProps={iconProps}
/>
);
} else {
leftIcon = (
<CardFeedIcon
selectedFeed={cardRow.bank}
iconProps={iconProps}
/>
);
}

return (
<View
testID={`your-spend-card-row-${cardRow.cardID}`}
Expand All @@ -56,18 +93,7 @@ function CardRow({cardRow, wrapperStyle}: CardRowProps) {
</View>
</View>
}
leftComponent={
<View style={[styles.justifyContentCenter, styles.h10]}>
<CardFeedIcon
isExpensifyCardFeed
iconProps={{
width: variables.cardIconWidth,
height: variables.cardIconHeight,
additionalStyles: [styles.overflowHidden, styles.br1],
}}
/>
</View>
}
leftComponent={<View style={[styles.justifyContentCenter, styles.h10]}>{leftIcon}</View>}
wrapperStyle={wrapperStyle}
hasSubMenuItems
shouldCheckActionAllowedOnPress={false}
Expand Down
2 changes: 1 addition & 1 deletion src/pages/home/YourSpendSection/SpendSummaryRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import {convertToDisplayString} from '@libs/CurrencyUtils';
import variables from '@styles/variables';
import type IconAsset from '@src/types/utils/IconAsset';
import YOUR_SPEND_ROW_STATE from './const';
import {YOUR_SPEND_ROW_STATE} from './const';
import type {useYourSpendData} from './useYourSpendData';

// Skeleton geometry mirrors `ForYouSection/ForYouSkeleton.tsx` so the home page
Expand Down
12 changes: 11 additions & 1 deletion src/pages/home/YourSpendSection/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,14 @@ const YOUR_SPEND_ROW_STATE = {
HIDDEN_EMPTY: 'hiddenEmpty',
} as const;

export default YOUR_SPEND_ROW_STATE;
/**
* Discriminator for a card row's artwork branch:
* - `EXPENSIFY` keeps the existing Expensify Card icon.
* - `THIRD_PARTY` switches to bank artwork (employer feed) or a Plaid institution icon.
*/
const YOUR_SPEND_CARD_KIND = {
EXPENSIFY: 'expensify',
THIRD_PARTY: 'thirdParty',
} as const;

export {YOUR_SPEND_ROW_STATE, YOUR_SPEND_CARD_KIND};
Loading
Loading