From ec7a6940dfed82c103b365324f3909b4b8f98311 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 11 Mar 2026 14:35:57 +0700 Subject: [PATCH 1/2] fix: Hide expired virtual Expensify cards from the Expensify Wallet --- src/libs/CardUtils.ts | 10 +++++++ .../settings/Wallet/PaymentMethodList.tsx | 4 ++- tests/unit/CardUtilsTest.ts | 29 +++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index ec161709ff400..db5523b8e457e 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -42,6 +42,7 @@ import type { import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type IconAsset from '@src/types/utils/IconAsset'; import {isBankAccountPartiallySetup} from './BankAccountUtils'; +import DateUtils from './DateUtils'; import {filterObject} from './ObjectUtils'; import {arePersonalDetailsMissing, getDisplayNameOrDefault} from './PersonalDetailsUtils'; import StringUtils from './StringUtils'; @@ -1392,6 +1393,14 @@ function getDisplayableExpensifyCards(cardList: CardList | undefined): Card[] { }); } +function isExpiredCard(card: Card): boolean { + if (!card.nameValuePairs?.validThru) { + return false; + } + const currentTime = DateUtils.getDBTime(); + return card.nameValuePairs.validThru < currentTime; +} + export { getAssignedCardSortKey, getDefaultExpensifyCardLimitType, @@ -1482,6 +1491,7 @@ export { isCardFrozen, isCardWithPotentialFraud, getDisplayableExpensifyCards, + isExpiredCard, }; export type {CompanyCardFeedIcons, CompanyCardBankIcons}; diff --git a/src/pages/settings/Wallet/PaymentMethodList.tsx b/src/pages/settings/Wallet/PaymentMethodList.tsx index 0b458f0778f11..febd8963e3db3 100644 --- a/src/pages/settings/Wallet/PaymentMethodList.tsx +++ b/src/pages/settings/Wallet/PaymentMethodList.tsx @@ -30,6 +30,7 @@ import { isCardFrozen, isExpensifyCard, isExpensifyCardPendingAction, + isExpiredCard, isPersonalCard, lastFourNumbersFromCardName, maskCardNumber, @@ -214,7 +215,8 @@ function PaymentMethodList({ (card) => CONST.EXPENSIFY_CARD.ACTIVE_STATES.includes(card.state ?? 0) && (isExpensifyCard(card) || !!card.domainName || isPersonalCard(card)) && - card.cardName !== CONST.COMPANY_CARDS.CARD_NAME.CASH, + card.cardName !== CONST.COMPANY_CARDS.CARD_NAME.CASH && + (!isExpensifyCard(card) || !isExpiredCard(card)), ); const assignedCardsSorted = lodashSortBy(assignedCards, getAssignedCardSortKey); diff --git a/tests/unit/CardUtilsTest.ts b/tests/unit/CardUtilsTest.ts index f3a7a5fc602b0..904d6e66f98b0 100644 --- a/tests/unit/CardUtilsTest.ts +++ b/tests/unit/CardUtilsTest.ts @@ -54,6 +54,7 @@ import { isDirectFeed as isDirectFeedCardUtils, isExpensifyCard, isExpensifyCardFullySetUp, + isExpiredCard, isMatchingCard, isPersonalCard, lastFourNumbersFromCardName, @@ -62,6 +63,7 @@ import { splitCardFeedWithDomainID, splitMaskedCardNumber, } from '@src/libs/CardUtils'; +import DateUtils from '@src/libs/DateUtils'; import type { BankAccountList, Card, @@ -3501,6 +3503,33 @@ describe('CardUtils', () => { expect(result?.domainName).toBe('legacy.com'); }); }); + + describe('isExpiredCard', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('returns false when validThru is missing', () => { + jest.spyOn(DateUtils, 'getDBTime').mockReturnValue('2026-02-25 00:00:00'); + expect(isExpiredCard({} as Card)).toBe(false); + expect(isExpiredCard({nameValuePairs: {}} as Card)).toBe(false); + }); + + it('returns true when validThru is before current time (UTC)', () => { + jest.spyOn(DateUtils, 'getDBTime').mockReturnValue('2026-02-25 00:00:00'); + expect(isExpiredCard({nameValuePairs: {validThru: '2026-02-24 23:59:59'}} as Card)).toBe(true); + }); + + it('returns false when validThru equals current time', () => { + jest.spyOn(DateUtils, 'getDBTime').mockReturnValue('2026-02-25 00:00:00'); + expect(isExpiredCard({nameValuePairs: {validThru: '2026-02-25 00:00:00'}} as Card)).toBe(false); + }); + + it('returns false when validThru is after current time', () => { + jest.spyOn(DateUtils, 'getDBTime').mockReturnValue('2026-02-25 00:00:00'); + expect(isExpiredCard({nameValuePairs: {validThru: '2026-02-25 00:00:01'}} as Card)).toBe(false); + }); + }); }); describe('formatMaskedCardName', () => { From 09fd29ea5ed6d40c61814bdcfcff2872de8acb6c Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 11 Mar 2026 16:45:25 +0700 Subject: [PATCH 2/2] update hasDisplayableAssignedCards --- src/libs/CardUtils.ts | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index db5523b8e457e..14d12ce273359 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -1315,6 +1315,17 @@ function generateCardID(): number { return Math.floor(Math.random() * 2 ** 21) * 2 ** 32 + Math.floor(Math.random() * 2 ** 32); } +/** + * Check if the card has expired + */ +function isExpiredCard(card: Card): boolean { + if (!card.nameValuePairs?.validThru) { + return false; + } + const currentTime = DateUtils.getDBTime(); + return card.nameValuePairs.validThru < currentTime; +} + /** * Check if there are any assigned cards that should be displayed in the wallet page. * This includes active Expensify cards, company cards (domain), and personal cards. @@ -1328,7 +1339,8 @@ function hasDisplayableAssignedCards(cardList: CardList | undefined): boolean { (card) => CONST.EXPENSIFY_CARD.ACTIVE_STATES.includes(card.state ?? 0) && (isExpensifyCard(card) || !!card.domainName || isPersonalCard(card)) && - card.cardName !== CONST.COMPANY_CARDS.CARD_NAME.CASH, + card.cardName !== CONST.COMPANY_CARDS.CARD_NAME.CASH && + (!isExpensifyCard(card) || !isExpiredCard(card)), ); } @@ -1393,14 +1405,6 @@ function getDisplayableExpensifyCards(cardList: CardList | undefined): Card[] { }); } -function isExpiredCard(card: Card): boolean { - if (!card.nameValuePairs?.validThru) { - return false; - } - const currentTime = DateUtils.getDBTime(); - return card.nameValuePairs.validThru < currentTime; -} - export { getAssignedCardSortKey, getDefaultExpensifyCardLimitType,