diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index f22965c6c3e5b..7fd317eabfe56 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'; @@ -1314,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. @@ -1327,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)), ); } @@ -1482,6 +1495,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 1cee08078c3df..7a859e19b612f 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 1f40b4c7093e2..b7d2e7316eaf4 100644 --- a/tests/unit/CardUtilsTest.ts +++ b/tests/unit/CardUtilsTest.ts @@ -55,6 +55,7 @@ import { isDirectFeed as isDirectFeedCardUtils, isExpensifyCard, isExpensifyCardFullySetUp, + isExpiredCard, isMatchingCard, isPersonalCard, lastFourNumbersFromCardName, @@ -63,6 +64,7 @@ import { splitCardFeedWithDomainID, splitMaskedCardNumber, } from '@src/libs/CardUtils'; +import DateUtils from '@src/libs/DateUtils'; import type { BankAccountList, Card, @@ -3503,6 +3505,33 @@ describe('CardUtils', () => { }); }); + 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('getFeedConnectionBrokenCard', () => { it('Should return undefined when feedCards is undefined', () => { expect(getFeedConnectionBrokenCard(undefined)).toBeUndefined();