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
1 change: 1 addition & 0 deletions assets/images/freeze-card.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,7 @@ const CONST = {
PAY_INVOICE_VIA_EXPENSIFY: 'payInvoiceViaExpensify',
PERSONAL_CARD_IMPORT: 'personalCardImport',
SUGGESTED_FOLLOWUPS: 'suggestedFollowups',
FREEZE_CARD: 'freezeCard',
},
BUTTON_STATES: {
DEFAULT: 'default',
Expand Down
2 changes: 2 additions & 0 deletions src/components/Icon/Expensicons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import FlagLevelOne from '@assets/images/flag_level_01.svg';
import FlagLevelTwo from '@assets/images/flag_level_02.svg';
import FlagLevelThree from '@assets/images/flag_level_03.svg';
import Folder from '@assets/images/folder.svg';
import FreezeCard from '@assets/images/freeze-card.svg';
import Fullscreen from '@assets/images/fullscreen.svg';
import GalleryNotFound from '@assets/images/gallery-not-found.svg';
import GalleryPlus from '@assets/images/gallery-plus.svg';
Expand Down Expand Up @@ -328,4 +329,5 @@ export {
CertiniaSquare,
ZenefitsSquare,
GalleryPlus,
FreezeCard,
};
2 changes: 2 additions & 0 deletions src/components/Icon/chunks/expensify-icons.chunk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ import FlagLevelOne from '@assets/images/flag_level_01.svg';
import FlagLevelTwo from '@assets/images/flag_level_02.svg';
import FlagLevelThree from '@assets/images/flag_level_03.svg';
import Folder from '@assets/images/folder.svg';
import FreezeCard from '@assets/images/freeze-card.svg';
import Fullscreen from '@assets/images/fullscreen.svg';
import GalleryNotFound from '@assets/images/gallery-not-found.svg';
import Gallery from '@assets/images/gallery.svg';
Expand Down Expand Up @@ -324,6 +325,7 @@ const Expensicons = {
FlagLevelTwo,
FlagLevelThree,
Fingerprint,
FreezeCard,
Fullscreen,
Folder,
Tag,
Expand Down
1 change: 1 addition & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2244,6 +2244,7 @@ const translations: TranslationDeepObject<typeof en> = {

${amount} für ${merchant} – ${date}`,
},
freezeCard: 'Karte sperren',
},
workflowsPage: {
workflowTitle: 'Ausgaben',
Expand Down
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2257,6 +2257,7 @@ const translations = {
alertMessage: ({cardLastFour, amount, merchant, date}: {cardLastFour: string; amount: string; merchant: string; date: string}) =>
`identified suspicious activity on card ending in ${cardLastFour}. Do you recognize this charge?\n\n${amount} for ${merchant} - ${date}`,
},
freezeCard: 'Freeze card',
},
workflowsPage: {
workflowTitle: 'Spend',
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2034,6 +2034,7 @@ const translations: TranslationDeepObject<typeof en> = {

${amount} para ${merchant} - ${date}`,
},
freezeCard: 'Congelar tarjeta',
},
workflowsPage: {
workflowTitle: 'Gasto',
Expand Down
1 change: 1 addition & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2250,6 +2250,7 @@ const translations: TranslationDeepObject<typeof en> = {

${amount} pour ${merchant} - ${date}`,
},
freezeCard: 'Geler la carte',
},
workflowsPage: {
workflowTitle: 'Dépense',
Expand Down
1 change: 1 addition & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2238,6 +2238,7 @@ const translations: TranslationDeepObject<typeof en> = {

${amount} per ${merchant} - ${date}`,
},
freezeCard: 'Blocca carta',
},
workflowsPage: {
workflowTitle: 'Spesa',
Expand Down
1 change: 1 addition & 0 deletions src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2225,6 +2225,7 @@ const translations: TranslationDeepObject<typeof en> = {

${date} の ${merchant} への ${amount}`,
},
freezeCard: 'カードを一時停止',
},
workflowsPage: {
workflowTitle: '支出',
Expand Down
1 change: 1 addition & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2239,6 +2239,7 @@ const translations: TranslationDeepObject<typeof en> = {

${amount} voor ${merchant} - ${date}`,
},
freezeCard: 'Kaart blokkeren',
},
workflowsPage: {
workflowTitle: 'Uitgaven',
Expand Down
1 change: 1 addition & 0 deletions src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2233,6 +2233,7 @@ const translations: TranslationDeepObject<typeof en> = {

${amount} dla ${merchant} - ${date}`,
},
freezeCard: 'Zamroź kartę',
},
workflowsPage: {
workflowTitle: 'Wydatki',
Expand Down
1 change: 1 addition & 0 deletions src/languages/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2232,6 +2232,7 @@ const translations: TranslationDeepObject<typeof en> = {

${amount} para ${merchant} - ${date}`,
},
freezeCard: 'Bloquear cartão',
},
workflowsPage: {
workflowTitle: 'Gastos',
Expand Down
1 change: 1 addition & 0 deletions src/languages/zh-hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2202,6 +2202,7 @@ const translations: TranslationDeepObject<typeof en> = {

${amount},商户:${merchant} - 日期:${date}`,
},
freezeCard: '冻结卡片',
},
workflowsPage: {
workflowTitle: '支出',
Expand Down
5 changes: 5 additions & 0 deletions src/libs/CardUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1160,6 +1160,10 @@ function hasDisplayableAssignedCards(cardList: CardList | undefined): boolean {
);
}

function isCardFrozen(card?: OnyxEntry<Card>): boolean {
return card?.state === CONST.EXPENSIFY_CARD.STATE.STATE_SUSPENDED && card?.nameValuePairs?.frozen != null;
Comment thread
ShridharGoel marked this conversation as resolved.
}

export {
getAssignedCardSortKey,
getDefaultExpensifyCardLimitType,
Expand Down Expand Up @@ -1240,6 +1244,7 @@ export {
isCardAlreadyAssigned,
generateCardID,
hasDisplayableAssignedCards,
isCardFrozen,
isCardWithPotentialFraud,
};

Expand Down
19 changes: 16 additions & 3 deletions src/pages/settings/Wallet/ExpensifyCardPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type {LocalizedTranslate} from '@components/LocaleContextProvider';
import {LockedAccountContext} from '@components/LockedAccountModalProvider';
import MenuItem from '@components/MenuItem';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import {usePersonalDetails} from '@components/OnyxListItemProvider';
import {usePersonalDetails, useSession} from '@components/OnyxListItemProvider';
import ScreenWrapper from '@components/ScreenWrapper';
import ScrollView from '@components/ScrollView';
import useCurrencyList from '@hooks/useCurrencyList';
Expand All @@ -24,9 +24,10 @@ import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useOnyx from '@hooks/useOnyx';
import usePermissions from '@hooks/usePermissions';
import useThemeStyles from '@hooks/useThemeStyles';
import {resetValidateActionCodeSent} from '@libs/actions/User';
import {formatCardExpiration, getDomainCards, getTranslationKeyForLimitType, maskCard, maskPin} from '@libs/CardUtils';
import {formatCardExpiration, getDomainCards, getTranslationKeyForLimitType, isCardFrozen, maskCard, maskPin} from '@libs/CardUtils';
import {convertToDisplayString, getCurrencyKeyByCountryCode} from '@libs/CurrencyUtils';
import DateUtils from '@libs/DateUtils';
import Navigation from '@libs/Navigation/Navigation';
Expand Down Expand Up @@ -115,7 +116,7 @@ function ExpensifyCardPage({route}: ExpensifyCardPageProps) {
const pageTitle = shouldDisplayCardDomain ? expensifyCardTitle : (cardList?.[cardID]?.nameValuePairs?.cardTitle ?? expensifyCardTitle);
const {displayName} = useCurrentUserPersonalDetails();
const personalDetails = usePersonalDetails();
const expensifyIcons = useMemoizedLazyExpensifyIcons(['Flag', 'MoneySearch']);
const expensifyIcons = useMemoizedLazyExpensifyIcons(['Flag', 'MoneySearch', 'FreezeCard']);

const [isNotFound, setIsNotFound] = useState(false);
const cardsToShow = useMemo(() => {
Expand Down Expand Up @@ -165,6 +166,11 @@ function ExpensifyCardPage({route}: ExpensifyCardPageProps) {

const isSignedInAsDelegate = !!account?.delegatedAccess?.delegate || false;

const session = useSession();
const isCardHolder = currentCard?.accountID === session?.accountID;

const {isBetaEnabled} = usePermissions();

if (isNotFound) {
return <NotFoundPage onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_WALLET)} />;
}
Expand Down Expand Up @@ -416,6 +422,13 @@ function ExpensifyCardPage({route}: ExpensifyCardPageProps) {
);
}}
/>
{isBetaEnabled(CONST.BETAS.FREEZE_CARD) && isCardHolder && !!currentCard && !isCardFrozen(currentCard) && (
<MenuItem
Comment thread
ShridharGoel marked this conversation as resolved.
icon={expensifyIcons.FreezeCard}
title={translate('cardPage.freezeCard')}
disabled={isOffline}
/>
)}
</>
)}
{cardToAdd !== undefined && (
Expand Down
43 changes: 43 additions & 0 deletions tests/unit/CardUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
getYearFromExpirationDateString,
hasIssuedExpensifyCard,
hasOnlyOneCardToAssign,
isCardFrozen,
isCSVFeedOrExpensifyCard,
isCustomFeed as isCustomFeedCardUtils,
isDirectFeed as isDirectFeedCardUtils,
Expand Down Expand Up @@ -2627,6 +2628,48 @@ describe('CardUtils', () => {
});
});

describe('isCardFrozen', () => {
it('Should return true when card state is suspended and frozen property is set', () => {
const card = {
state: CONST.EXPENSIFY_CARD.STATE.STATE_SUSPENDED,
nameValuePairs: {
frozen: true,
},
} as unknown as Card;
expect(isCardFrozen(card)).toBe(true);
});

it('Should return false when card state is suspended but frozen property is missing', () => {
const card = {
state: CONST.EXPENSIFY_CARD.STATE.STATE_SUSPENDED,
nameValuePairs: {},
} as unknown as Card;
expect(isCardFrozen(card)).toBe(false);
});

it('Should return false when card state is not suspended but frozen property is set', () => {
const card = {
state: CONST.EXPENSIFY_CARD.STATE.OPEN,
nameValuePairs: {
frozen: true,
},
} as unknown as Card;
expect(isCardFrozen(card)).toBe(false);
});

it('Should return false when card is undefined', () => {
expect(isCardFrozen(undefined)).toBe(false);
});

it('Should return false when card is valid but not suspended or frozen', () => {
const card = {
state: CONST.EXPENSIFY_CARD.STATE.OPEN,
nameValuePairs: {},
} as unknown as Card;
expect(isCardFrozen(card)).toBe(false);
});
});

describe('isMatchingCard', () => {
it('should match by encryptedCardNumber for exact match', () => {
const card: Card = {
Expand Down
Loading