Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/libs/CardUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -1450,6 +1451,22 @@ function getCardCurrency(card?: OnyxEntry<Card>, cardSettings?: OnyxEntry<Expens
return CONST.CURRENCY.USD;
}

function getCardHintText(validFrom: string | undefined, validThru: string | undefined, assigneeTimeZone: SelectedTimezone | undefined, translate: LocalizedTranslate) {
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});
}

export {
getAssignedCardSortKey,
getDefaultExpensifyCardLimitType,
Expand Down Expand Up @@ -1543,6 +1560,7 @@ export {
getDisplayableExpensifyCards,
isExpiredCard,
getCardCurrency,
getCardHintText,
};

export type {CompanyCardFeedIcons, CompanyCardBankIcons};
21 changes: 1 addition & 20 deletions src/pages/settings/Wallet/ExpensifyCardPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -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 =
Expand All @@ -75,22 +72,6 @@ function getLimitTypeTranslationKeys(limitType: ValueOf<typeof CONST.EXPENSIFY_C
}
}

const getCardHintText = (validFrom: string | undefined, validThru: string | undefined, assigneeTimeZone: SelectedTimezone | undefined, translate: LocalizedTranslate) => {
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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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)}
/>
</OfflineWithFeedback>
<OfflineWithFeedback pendingAction={card?.nameValuePairs?.pendingFields?.cardTitle}>
Expand Down
34 changes: 34 additions & 0 deletions tests/unit/CardUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
getCardDescription,
getCardFeedIcon,
getCardFeedWithDomainID,
getCardHintText,
getCardsByCardholderName,
getCardSettings,
getCompanyCardDescription,
Expand Down Expand Up @@ -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', () => {
Expand Down
Loading