diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 8f818ae66bb2..5ccf89ff717f 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -36,6 +36,23 @@ function getMonthFromExpirationDateString(expirationDateString: string) { return expirationDateString.substring(0, 2); } +/** + * Sorting logic for assigned cards. + * + * Ensure to sort physical Expensify cards first, no matter what their cardIDs are. + * This way ensures the Expensify Combo Card detail is rendered correctly, + * because we will always use the cardID of the physical card from the combo card duo. + * + * @param card - card to get the sort key for + * @returns number + */ +function getAssignedCardSortKey(card: Card): number { + if (!isExpensifyCard(card)) { + return 2; + } + return card?.nameValuePairs?.isVirtual ? 1 : 0; +} + /** * @param card * @returns boolean @@ -663,6 +680,7 @@ function getFundIdFromSettingsKey(key: string) { } export { + getAssignedCardSortKey, isExpensifyCard, getDomainCards, formatCardExpiration, diff --git a/src/pages/settings/Wallet/PaymentMethodList.tsx b/src/pages/settings/Wallet/PaymentMethodList.tsx index ebd5502d2fac..7fbc44f655bd 100644 --- a/src/pages/settings/Wallet/PaymentMethodList.tsx +++ b/src/pages/settings/Wallet/PaymentMethodList.tsx @@ -21,7 +21,7 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useThemeIllustrations from '@hooks/useThemeIllustrations'; import useThemeStyles from '@hooks/useThemeStyles'; import {clearAddPaymentMethodError, clearDeletePaymentMethodError} from '@libs/actions/PaymentMethods'; -import {getCardFeedIcon, getPlaidInstitutionIconUrl, isExpensifyCard, lastFourNumbersFromCardName, maskCardNumber} from '@libs/CardUtils'; +import {getAssignedCardSortKey, getCardFeedIcon, getPlaidInstitutionIconUrl, isExpensifyCard, lastFourNumbersFromCardName, maskCardNumber} from '@libs/CardUtils'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import {formatPaymentMethods} from '@libs/PaymentUtils'; @@ -219,7 +219,8 @@ function PaymentMethodList({ const assignedCards = Object.values(isLoadingCardList ? {} : (cardList ?? {})) // Filter by active cards associated with a domain .filter((card) => !!card.domainName && CONST.EXPENSIFY_CARD.ACTIVE_STATES.includes(card.state ?? 0)); - const assignedCardsSorted = lodashSortBy(assignedCards, (card) => !isExpensifyCard(card)); + + const assignedCardsSorted = lodashSortBy(assignedCards, getAssignedCardSortKey); const assignedCardsGrouped: PaymentMethodItem[] = []; assignedCardsSorted.forEach((card) => { diff --git a/tests/unit/CardUtilsTest.ts b/tests/unit/CardUtilsTest.ts index 546503348248..79176759540b 100644 --- a/tests/unit/CardUtilsTest.ts +++ b/tests/unit/CardUtilsTest.ts @@ -1,3 +1,4 @@ +import lodashSortBy from 'lodash/sortBy'; import type {OnyxCollection} from 'react-native-onyx'; import type IllustrationsType from '@styles/theme/illustrations/types'; import type * as Illustrations from '@src/components/Icon/Illustrations'; @@ -8,6 +9,7 @@ import { filterInactiveCards, flatAllCardsList, formatCardExpiration, + getAssignedCardSortKey, getBankCardDetailsImage, getBankName, getCardDescription, @@ -1138,4 +1140,27 @@ describe('CardUtils', () => { expect(description).toBe('Test'); }); }); + + describe('Expensify card sort comparator', () => { + it('should not change the order of non-Expensify cards', () => { + const cardList = { + 10: {cardID: 10, bank: 'chase'}, // non-Expensify + 11: {cardID: 11, bank: 'chase'}, // non-Expensify + } as unknown as CardList; + + const sorted = lodashSortBy(Object.values(cardList), getAssignedCardSortKey); + expect(sorted.map((r: Card) => r.cardID)).toEqual([10, 11]); + }); + + it('places physical Expensify card before its virtual sibling', () => { + const cardList = { + 10: {cardID: 10, bank: CONST.EXPENSIFY_CARD.BANK, nameValuePairs: {isVirtual: true}}, // Expensify virtual + 11: {cardID: 11, bank: CONST.EXPENSIFY_CARD.BANK}, // Expensify physical + 99: {cardID: 99, bank: 'chase'}, // non-Expensify + } as unknown as CardList; + + const sorted = lodashSortBy(Object.values(cardList), getAssignedCardSortKey); + expect(sorted.map((r: Card) => r.cardID)).toEqual([11, 10, 99]); + }); + }); });