diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index d1ea19a379056..a84f25d3940d0 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -39,6 +39,7 @@ import type { CompanyFeeds, NonConnectableBankName, } from '@src/types/onyx/CardFeeds'; +import type {SelectedTimezone} from '@src/types/onyx/PersonalDetails'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type IconAsset from '@src/types/utils/IconAsset'; import {isBankAccountPartiallySetup} from './BankAccountUtils'; @@ -1450,6 +1451,22 @@ function getCardCurrency(card?: OnyxEntry, cardSettings?: OnyxEntry { + const dateInTimezone = DateUtils.formatUTCDateTimeToDateInTimezone(utcDateTime, assigneeTimeZone); + return dateInTimezone ? DateUtils.formatToReadableString(dateInTimezone) : ''; + }; + const startDate = formatDateForDisplay(validFrom); + const endDate = formatDateForDisplay(validThru); + if (!startDate || !endDate) { + return; + } + return translate('workspace.card.issueNewCard.validFromTo', {startDate, endDate}); +} + export { getAssignedCardSortKey, getDefaultExpensifyCardLimitType, @@ -1543,6 +1560,7 @@ export { getDisplayableExpensifyCards, isExpiredCard, getCardCurrency, + getCardHintText, }; export type {CompanyCardFeedIcons, CompanyCardBankIcons}; diff --git a/src/pages/settings/Wallet/ExpensifyCardPage/index.tsx b/src/pages/settings/Wallet/ExpensifyCardPage/index.tsx index 10c2a3a0973fc..bddd7666d95e5 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage/index.tsx +++ b/src/pages/settings/Wallet/ExpensifyCardPage/index.tsx @@ -12,7 +12,6 @@ import DotIndicatorMessage from '@components/DotIndicatorMessage'; import FormHelpMessage from '@components/FormHelpMessage'; import FrozenCardHeader from '@components/FrozenCardHeader'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import type {LocalizedTranslate} from '@components/LocaleContextProvider'; import {useLockedAccountActions, useLockedAccountState} from '@components/LockedAccountModalProvider'; import MenuItem from '@components/MenuItem'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; @@ -30,9 +29,8 @@ import usePermissions from '@hooks/usePermissions'; import useThemeStyles from '@hooks/useThemeStyles'; import {freezeCard, unfreezeCard} from '@libs/actions/Card'; import {resetValidateActionCodeSent} from '@libs/actions/User'; -import {formatCardExpiration, getCardCurrency, getDomainCards, getTranslationKeyForLimitType, isCardFrozen, maskCard, maskPin} from '@libs/CardUtils'; +import {formatCardExpiration, getCardCurrency, getCardHintText, getDomainCards, getTranslationKeyForLimitType, isCardFrozen, maskCard, maskPin} from '@libs/CardUtils'; import {convertToDisplayString} from '@libs/CurrencyUtils'; -import DateUtils from '@libs/DateUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {DomainCardNavigatorParamList, SettingsNavigatorParamList} from '@libs/Navigation/types'; @@ -48,7 +46,6 @@ import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; -import type {SelectedTimezone} from '@src/types/onyx/PersonalDetails'; import {useExpensifyCardActions, useExpensifyCardState} from './ExpensifyCardContextProvider'; type ExpensifyCardPageProps = @@ -75,22 +72,6 @@ function getLimitTypeTranslationKeys(limitType: ValueOf { - if (!validFrom || !validThru) { - return; - } - const formatDateForDisplay = (utcDateTime: string): string => { - const dateInTimezone = DateUtils.formatUTCDateTimeToDateInTimezone(utcDateTime, assigneeTimeZone); - return dateInTimezone ? DateUtils.formatToReadableString(dateInTimezone) : ''; - }; - const startDate = formatDateForDisplay(validFrom); - const endDate = formatDateForDisplay(validThru); - if (!startDate || !endDate) { - return; - } - return translate('workspace.card.issueNewCard.validFromTo', {startDate, endDate}); -}; - function ExpensifyCardPage({route}: ExpensifyCardPageProps) { const {cardID} = route.params; const [account] = useOnyx(ONYXKEYS.ACCOUNT); diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx index 358a886bb253e..1c07aa648948a 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx @@ -26,7 +26,7 @@ import usePermissions from '@hooks/usePermissions'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; -import {getAllCardsForWorkspace, getTranslationKeyForLimitType, isCardFrozen, maskCard} from '@libs/CardUtils'; +import {getAllCardsForWorkspace, getCardHintText, getTranslationKeyForLimitType, isCardFrozen, maskCard} from '@libs/CardUtils'; import {convertToDisplayString} from '@libs/CurrencyUtils'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; @@ -262,6 +262,7 @@ function WorkspaceExpensifyCardDetailsPage({route}: WorkspaceExpensifyCardDetail : ROUTES.EXPENSIFY_CARD_LIMIT_TYPE.getRoute(policyID, cardID, Navigation.getActiveRoute()), ) } + hintText={getCardHintText(card?.nameValuePairs?.validFrom, card?.nameValuePairs?.validThru, cardholder?.timezone?.selected, translate)} /> diff --git a/tests/unit/CardUtilsTest.ts b/tests/unit/CardUtilsTest.ts index 622ab8c6cdb18..2b31f2194e8a7 100644 --- a/tests/unit/CardUtilsTest.ts +++ b/tests/unit/CardUtilsTest.ts @@ -25,6 +25,7 @@ import { getCardDescription, getCardFeedIcon, getCardFeedWithDomainID, + getCardHintText, getCardsByCardholderName, getCardSettings, getCompanyCardDescription, @@ -3777,6 +3778,39 @@ describe('CardUtils', () => { expect(getFeedConnectionBrokenCard(feedCards, 'oauth.chase.com')).toBeUndefined(); }); }); + + describe('getCardHintText', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('returns undefined when validFrom or validThru is missing', () => { + const translate = jest.fn(); + + expect(getCardHintText(undefined, '2026-02-25 00:00:00', undefined, translate as never)).toBeUndefined(); + expect(getCardHintText('2026-02-25 00:00:00', undefined, undefined, translate as never)).toBeUndefined(); + expect(translate).not.toHaveBeenCalled(); + }); + + it('returns undefined when date formatting fails', () => { + const translate = jest.fn(); + jest.spyOn(DateUtils, 'formatUTCDateTimeToDateInTimezone').mockReturnValue('' as never); + + expect(getCardHintText('2026-02-01 00:00:00', '2026-02-25 00:00:00', {} as never, translate as never)).toBeUndefined(); + expect(translate).not.toHaveBeenCalled(); + }); + + it('returns translated hint text when both dates are formatted', () => { + const translate = jest.fn().mockReturnValue('translated'); + jest.spyOn(DateUtils, 'formatUTCDateTimeToDateInTimezone').mockReturnValue({} as never); + jest.spyOn(DateUtils, 'formatToReadableString').mockReturnValueOnce('Feb 1, 2026').mockReturnValueOnce('Feb 25, 2026'); + + const result = getCardHintText('2026-02-01 00:00:00', '2026-02-25 00:00:00', {} as never, translate as never); + + expect(result).toBe('translated'); + expect(translate).toHaveBeenCalledWith('workspace.card.issueNewCard.validFromTo', {startDate: 'Feb 1, 2026', endDate: 'Feb 25, 2026'}); + }); + }); }); describe('formatMaskedCardName', () => {