From 44011fcf4a693758eadcff55caca10ae19a09c34 Mon Sep 17 00:00:00 2001 From: cosmicvulpes Date: Fri, 8 Aug 2025 13:36:54 +0530 Subject: [PATCH 1/7] fix: implement physical-first sorting for Expensify cards in PaymentMethodList - Create getAssignedCardSortKey utility function in CardUtils.ts - Replace simple boolean comparator with new sorting logic - Add comprehensive unit tests for card sorting scenarios - Resolves issue #68033 where physical cards were hidden in navigation - Physical Expensify cards now appear before virtual ones irrespective of their cardID --- src/libs/CardUtils.ts | 8 ++++++ .../settings/Wallet/PaymentMethodList.tsx | 5 ++-- tests/unit/CardUtilsTest.ts | 27 +++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 8f818ae66bb2..60555b9dfaee 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -40,6 +40,13 @@ function getMonthFromExpirationDateString(expirationDateString: string) { * @param card * @returns boolean */ +function getAssignedCardSortKey(card: Card): number { + if (!isExpensifyCard(card)) { + return 2; + } + return card?.nameValuePairs?.isVirtual ? 1 : 0; +} + function isExpensifyCard(card?: Card) { if (!card) { return false; @@ -663,6 +670,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..83c772c27126 100644 --- a/tests/unit/CardUtilsTest.ts +++ b/tests/unit/CardUtilsTest.ts @@ -3,11 +3,13 @@ import type IllustrationsType from '@styles/theme/illustrations/types'; import type * as Illustrations from '@src/components/Icon/Illustrations'; import CONST from '@src/CONST'; import IntlStore from '@src/languages/IntlStore'; +import lodashSortBy from 'lodash/sortBy'; import { checkIfFeedConnectionIsBroken, filterInactiveCards, flatAllCardsList, formatCardExpiration, + getAssignedCardSortKey, getBankCardDetailsImage, getBankName, getCardDescription, @@ -33,6 +35,8 @@ import type {Card, CardFeeds, CardList, CompanyCardFeed, ExpensifyCardSettings, import type {CompanyCardFeedWithNumber} from '@src/types/onyx/CardFeeds'; import {localeCompare} from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; const shortDate = '0924'; const shortDateSlashed = '09/24'; @@ -1138,4 +1142,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: any) => r.cardID)).toEqual([10, 11]); + }); + + it('places physical Expensify card before its virtual sibling', async () => { + 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: any) => r.cardID)).toEqual([11, 10, 99]); + }); + }); }); From 5992f14b0894d3657e3ab37eb746c8dd15e2fb25 Mon Sep 17 00:00:00 2001 From: cosmicvulpes Date: Fri, 8 Aug 2025 14:22:04 +0530 Subject: [PATCH 2/7] fix lint issue in imports order --- tests/unit/CardUtilsTest.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/unit/CardUtilsTest.ts b/tests/unit/CardUtilsTest.ts index 83c772c27126..ca11eaea31e1 100644 --- a/tests/unit/CardUtilsTest.ts +++ b/tests/unit/CardUtilsTest.ts @@ -1,9 +1,9 @@ +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'; import CONST from '@src/CONST'; import IntlStore from '@src/languages/IntlStore'; -import lodashSortBy from 'lodash/sortBy'; import { checkIfFeedConnectionIsBroken, filterInactiveCards, @@ -35,8 +35,6 @@ import type {Card, CardFeeds, CardList, CompanyCardFeed, ExpensifyCardSettings, import type {CompanyCardFeedWithNumber} from '@src/types/onyx/CardFeeds'; import {localeCompare} from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; -import Onyx from 'react-native-onyx'; -import ONYXKEYS from '@src/ONYXKEYS'; const shortDate = '0924'; const shortDateSlashed = '09/24'; From 275463632fade5c3f02aabba737359808ebeb660 Mon Sep 17 00:00:00 2001 From: cosmicvulpes Date: Fri, 8 Aug 2025 14:48:07 +0530 Subject: [PATCH 3/7] fix lint issues related to object types --- tests/unit/CardUtilsTest.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/CardUtilsTest.ts b/tests/unit/CardUtilsTest.ts index ca11eaea31e1..79176759540b 100644 --- a/tests/unit/CardUtilsTest.ts +++ b/tests/unit/CardUtilsTest.ts @@ -1149,10 +1149,10 @@ describe('CardUtils', () => { } as unknown as CardList; const sorted = lodashSortBy(Object.values(cardList), getAssignedCardSortKey); - expect(sorted.map((r: any) => r.cardID)).toEqual([10, 11]); + expect(sorted.map((r: Card) => r.cardID)).toEqual([10, 11]); }); - it('places physical Expensify card before its virtual sibling', async () => { + 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 @@ -1160,7 +1160,7 @@ describe('CardUtils', () => { } as unknown as CardList; const sorted = lodashSortBy(Object.values(cardList), getAssignedCardSortKey); - expect(sorted.map((r: any) => r.cardID)).toEqual([11, 10, 99]); + expect(sorted.map((r: Card) => r.cardID)).toEqual([11, 10, 99]); }); }); }); From 383c557b30890bd89cf668e40ddfbed818ee01a5 Mon Sep 17 00:00:00 2001 From: cosmicvulpes Date: Fri, 8 Aug 2025 16:44:56 +0530 Subject: [PATCH 4/7] update new method's doc comment. --- src/libs/CardUtils.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 60555b9dfaee..034a1c5f8ba3 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -38,7 +38,7 @@ function getMonthFromExpirationDateString(expirationDateString: string) { /** * @param card - * @returns boolean + * @returns number */ function getAssignedCardSortKey(card: Card): number { if (!isExpensifyCard(card)) { @@ -47,6 +47,10 @@ function getAssignedCardSortKey(card: Card): number { return card?.nameValuePairs?.isVirtual ? 1 : 0; } +/** + * @param card + * @returns boolean + */ function isExpensifyCard(card?: Card) { if (!card) { return false; From c4965f75293ef8524d5d74860753d7f05f874c78 Mon Sep 17 00:00:00 2001 From: Sudesh Date: Mon, 11 Aug 2025 20:04:28 +0530 Subject: [PATCH 5/7] Update src/libs/CardUtils.ts Co-authored-by: Vit Horacek <36083550+mountiny@users.noreply.github.com> --- src/libs/CardUtils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 034a1c5f8ba3..3a1c8135517a 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -37,6 +37,7 @@ function getMonthFromExpirationDateString(expirationDateString: string) { } /** + * 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 * @returns number */ From 971c972038328734840841175044c240de3db67b Mon Sep 17 00:00:00 2001 From: cosmicvulpes Date: Mon, 11 Aug 2025 20:08:39 +0530 Subject: [PATCH 6/7] update getAssignedCardSortKey method description --- src/libs/CardUtils.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 3a1c8135517a..13791f8e53f2 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -37,8 +37,13 @@ function getMonthFromExpirationDateString(expirationDateString: string) { } /** - * 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 + * 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 { From 8ef200b55d401ed81825473e9335ee45e32079f5 Mon Sep 17 00:00:00 2001 From: cosmicvulpes Date: Mon, 11 Aug 2025 20:14:56 +0530 Subject: [PATCH 7/7] remove unwanted whitespace --- src/libs/CardUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 13791f8e53f2..5ccf89ff717f 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -38,7 +38,7 @@ function getMonthFromExpirationDateString(expirationDateString: string) { /** * 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.