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
6 changes: 6 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ const cardActiveStates: number[] = [2, 3, 4, 7];

const brokenConnectionScrapeStatuses: number[] = [200, 434, 531, 530, 500, 666];

const reauthScrapeStatuses: number[] = [438, 532];

// Hide not issued or not activated cards (states 2, 4) from card filter options in search, as no transactions can be made on cards in these states
const cardHiddenFromSearchStates: number[] = [2, 4];

Expand Down Expand Up @@ -4839,6 +4841,10 @@ const CONST = {
},
COMPANY_CARDS: {
BROKEN_CONNECTION_IGNORED_STATUSES: brokenConnectionScrapeStatuses,

// Scrape result codes where the connection is broken because the user needs to re-authenticate with their bank
REAUTH_SCRAPE_STATUSES: reauthScrapeStatuses,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
REAUTH_SCRAPE_STATUSES: reauthScrapeStatuses,
REAUTH_SCRAPE_STATUSES: reauthScrapeStatuses,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Applied in 68b0d0f. ✅


// After a card connection has been broken and unresolved for this many days, stop actively
// prompting the user: the time-sensitive home task and the RBR are removed (the error itself is kept).
BROKEN_CONNECTION_DISMISS_AFTER_DAYS: 90,
Expand Down
1 change: 1 addition & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2589,6 +2589,7 @@ const translations: TranslationDeepObject<typeof en> = {
fixConnection: 'Bitte beheben Sie diese Verbindung',
fixConnectionIn: (companyCardsRoute: string) => `Bitte beheben Sie diese Verbindung in <a href="${companyCardsRoute}">Firmenkarten</a>`,
askAdminToFixConnection: 'Bitte bitten Sie eine(n) Admin, diese Verbindung zu reparieren',
reconnectBank: 'Ihre Bankverbindung muss erneut authentifiziert werden',
},
bankAccountStatus: {
active: 'Aktiv',
Expand Down
1 change: 1 addition & 0 deletions src/languages/el.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2649,6 +2649,7 @@ const translations: TranslationDeepObject<typeof en> = {
fixConnection: 'Παρακαλούμε διορθώστε αυτήν τη σύνδεση',
fixConnectionIn: (companyCardsRoute: string) => `Παρακαλούμε διορθώστε αυτήν τη σύνδεση στις <a href="${companyCardsRoute}">εταιρικές κάρτες</a>`,
askAdminToFixConnection: 'Παρακαλούμε ζητήστε από έναν διαχειριστή να διορθώσει αυτήν τη σύνδεση',
reconnectBank: 'Η σύνδεσή σας με την τράπεζα χρειάζεται επαναπιστοποίηση',
},
bankAccountStatus: {
active: 'Ενεργό',
Expand Down
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2648,6 +2648,7 @@ const translations = {
fixConnection: 'Please fix this connection',
fixConnectionIn: (companyCardsRoute: string) => `Please fix this connection in <a href="${companyCardsRoute}">company cards</a>`,
askAdminToFixConnection: 'Please ask an admin to fix this connection',
reconnectBank: 'Your bank connection needs to be re-authenticated',
},
bankAccountStatus: {
active: 'Active',
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2510,6 +2510,7 @@ const translations: TranslationDeepObject<typeof en> = {
fixConnection: 'Por favor, corrige esta conexión',
fixConnectionIn: (companyCardsRoute) => `Por favor, corrige esta conexión en <a href="${companyCardsRoute}">tarjetas de la empresa</a>`,
askAdminToFixConnection: 'Pídele a un administrador que solucione esta conexión',
reconnectBank: 'Tu conexión bancaria necesita volver a autenticarse',
},
bankAccountStatus: {
active: 'Activo',
Expand Down
1 change: 1 addition & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2594,6 +2594,7 @@ const translations: TranslationDeepObject<typeof en> = {
fixConnection: 'Veuillez corriger cette connexion',
fixConnectionIn: (companyCardsRoute: string) => `Veuillez corriger cette connexion dans <a href="${companyCardsRoute}">cartes d’entreprise</a>`,
askAdminToFixConnection: 'Veuillez demander à un administrateur de corriger cette connexion',
reconnectBank: 'Votre connexion bancaire doit être réauthentifiée',
},
bankAccountStatus: {
active: 'Actif',
Expand Down
1 change: 1 addition & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2582,6 +2582,7 @@ const translations: TranslationDeepObject<typeof en> = {
fixConnection: 'Sistema questa connessione per favore',
fixConnectionIn: (companyCardsRoute: string) => `Correggi questa connessione in <a href="${companyCardsRoute}">carte aziendali</a>`,
askAdminToFixConnection: 'Chiedi a un amministratore di correggere questa connessione',
reconnectBank: 'La connessione con la tua banca deve essere nuovamente autenticata',
},
bankAccountStatus: {
active: 'Attiva',
Expand Down
1 change: 1 addition & 0 deletions src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2559,6 +2559,7 @@ const translations: TranslationDeepObject<typeof en> = {
fixConnection: 'この接続を修正してください',
fixConnectionIn: (companyCardsRoute: string) => `この接続を<a href="${companyCardsRoute}">会社カード</a>で修正してください`,
askAdminToFixConnection: '管理者にこの接続の修正を依頼してください',
reconnectBank: '銀行連携の再認証が必要です',
},
bankAccountStatus: {
active: 'アクティブ',
Expand Down
1 change: 1 addition & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2583,6 +2583,7 @@ const translations: TranslationDeepObject<typeof en> = {
fixConnection: 'Repareer deze verbinding alsjeblieft',
fixConnectionIn: (companyCardsRoute: string) => `Repareer deze verbinding in <a href="${companyCardsRoute}">bedrijfskaarten</a>`,
askAdminToFixConnection: 'Vraag een/beheerder om deze verbinding te herstellen',
reconnectBank: 'Je bankverbinding moet opnieuw worden geverifieerd',
},
bankAccountStatus: {
active: 'Actief',
Expand Down
1 change: 1 addition & 0 deletions src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2575,6 +2575,7 @@ const translations: TranslationDeepObject<typeof en> = {
fixConnection: 'Napraw to połączenie',
fixConnectionIn: (companyCardsRoute: string) => `Napraw to połączenie w <a href="${companyCardsRoute}">firmowe karty</a>`,
askAdminToFixConnection: 'Poproś administratora o naprawienie tego połączenia',
reconnectBank: 'Twoje połączenie z bankiem wymaga ponownego uwierzytelnienia',
},
bankAccountStatus: {
active: 'Aktywne',
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 @@ -2576,6 +2576,7 @@ const translations: TranslationDeepObject<typeof en> = {
fixConnection: 'Corrija esta conexão',
fixConnectionIn: (companyCardsRoute: string) => `Corrija esta conexão em <a href="${companyCardsRoute}">cartões corporativos</a>`,
askAdminToFixConnection: 'Peça para um administrador corrigir essa conexão',
reconnectBank: 'Sua conexão bancária precisa ser autenticada novamente',
},
bankAccountStatus: {
active: 'Ativo',
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 @@ -2490,6 +2490,7 @@ const translations: TranslationDeepObject<typeof en> = {
fixConnection: '请修复此连接',
fixConnectionIn: (companyCardsRoute: string) => `请在<a href="${companyCardsRoute}">公司卡</a>中修复此连接`,
askAdminToFixConnection: '请联系管理员修复此连接',
reconnectBank: '您的银行连接需要重新验证',
},
bankAccountStatus: {
active: '活跃',
Expand Down
18 changes: 18 additions & 0 deletions src/libs/CardUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ type CardConnectionStatusDisplay = {
actionKey?: TranslationPaths;
shouldUsePersonalCardFix?: boolean;
shouldUseCompanyCardsLink?: boolean;
shouldUseReauthMessage?: boolean;
};

type CardConnectionStatusDisplayParams = {
Expand All @@ -137,6 +138,7 @@ type CardConnectionStatusDisplayParams = {
isCardInactive: boolean;
isPersonalCard: boolean;
isAdminForCardPolicy: boolean;
doesCardNeedReauthentication?: boolean;
policyID?: string;
};

Expand Down Expand Up @@ -1369,13 +1371,24 @@ function isCardConnectionBroken(card: Card): boolean {
return !!card.lastScrapeResult && !CONST.COMPANY_CARDS.BROKEN_CONNECTION_IGNORED_STATUSES.includes(card.lastScrapeResult);
}

/**
* Check if the card connection is broken specifically because the user needs to re-authenticate with their bank
*
* @param card the card to check
* @returns true if the connection needs re-authentication, false otherwise
*/
function doesCardConnectionNeedReauthentication(card: Card): boolean {
return isCardConnectionBroken(card) && !!card.lastScrapeResult && CONST.COMPANY_CARDS.REAUTH_SCRAPE_STATUSES.includes(card.lastScrapeResult);
}

function getCardConnectionStatusDisplay({
shouldShowConnectionStatus,
isCardBroken,
shouldShowRBR,
isCardInactive: isCardInactiveStatus,
isPersonalCard: isPersonalCardStatus,
isAdminForCardPolicy,
doesCardNeedReauthentication,
policyID,
}: CardConnectionStatusDisplayParams): CardConnectionStatusDisplay | undefined {
if (!shouldShowConnectionStatus) {
Expand All @@ -1385,11 +1398,14 @@ function getCardConnectionStatusDisplay({
const shouldShowMessage = isCardBroken || shouldShowRBR || isCardInactiveStatus;
const shouldUsePersonalCardFix = shouldShowMessage && isPersonalCardStatus;
const shouldUseCompanyCardsLink = shouldShowMessage && !isPersonalCardStatus && isAdminForCardPolicy && !!policyID;
const shouldUseReauthMessage = shouldShowMessage && !!doesCardNeedReauthentication && isPersonalCardStatus;
let messageKey: TranslationPaths | undefined;

if (shouldShowMessage) {
if (shouldUseCompanyCardsLink) {
messageKey = 'walletPage.cardStatus.fixConnectionIn';
} else if (shouldUseReauthMessage) {
messageKey = 'walletPage.cardStatus.reconnectBank';
} else if (isPersonalCardStatus) {
messageKey = 'walletPage.cardStatus.fixConnection';
} else {
Expand All @@ -1404,6 +1420,7 @@ function getCardConnectionStatusDisplay({
actionKey: shouldUsePersonalCardFix ? 'common.actionBadge.fix' : undefined,
shouldUsePersonalCardFix,
shouldUseCompanyCardsLink,
shouldUseReauthMessage,
};
}

Expand Down Expand Up @@ -2132,6 +2149,7 @@ export {
getCSVFeedType,
getFeedType,
isCardConnectionBroken,
doesCardConnectionNeedReauthentication,
getCardConnectionStatusDisplay,
isBrokenConnectionPastDismissThreshold,
isSmartLimitEnabled,
Expand Down
4 changes: 4 additions & 0 deletions src/pages/settings/Wallet/PaymentMethodList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
isActionableVirtualExpensifyCard,
isBrokenConnectionPastDismissThreshold,
isCardConnectionBroken,
doesCardConnectionNeedReauthentication,
isCardFrozen,
isCardInactive,
isExpensifyCard,
Expand Down Expand Up @@ -322,6 +323,7 @@ function PaymentMethodList({
isCardInactive: isCardInactiveState,
isPersonalCard: isUserPersonalCard,
isAdminForCardPolicy,
doesCardNeedReauthentication: doesCardConnectionNeedReauthentication(card),
policyID: policyIDForCard,
});
const shouldShowCardConnectionMessage = !!cardConnectionStatusDisplay?.messageKey;
Expand All @@ -340,6 +342,8 @@ function PaymentMethodList({
let cardConnectionMessage: string | undefined;
if (cardConnectionStatusDisplay.shouldUseCompanyCardsLink && policyIDForCard) {
cardConnectionMessage = translate('walletPage.cardStatus.fixConnectionIn', `${environmentURL}/${ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyIDForCard)}`);
} else if (cardConnectionStatusDisplay.shouldUseReauthMessage) {
cardConnectionMessage = translate('walletPage.cardStatus.reconnectBank');
} else if (cardConnectionStatusDisplay.shouldUsePersonalCardFix) {
cardConnectionMessage = translate('walletPage.cardStatus.fixConnection');
} else if (cardConnectionStatusDisplay.messageKey) {
Expand Down
9 changes: 7 additions & 2 deletions src/pages/settings/Wallet/PersonalCardDetailsHeaderMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import ROUTES from '@src/ROUTES';
import type {Card, PersonalDetails} from '@src/types/onyx';
import type IconAsset from '@src/types/utils/IconAsset';

import {format, parseISO} from 'date-fns';
import {format, isValid, parseISO} from 'date-fns';
import React from 'react';
import {View} from 'react-native';

Expand Down Expand Up @@ -57,6 +57,11 @@ function PersonalCardDetailsHeaderMenu({
const styles = useThemeStyles();
const icons = useMemoizedLazyExpensifyIcons(['Table', 'Trashcan']);

// Guard against an invalid scrapeMinDate, since`format` throws `RangeError: Invalid time value`
// when `parseISO` can't parse the value
const parsedScrapeMinDate = card?.scrapeMinDate ? parseISO(card.scrapeMinDate) : undefined;
const transactionStartDateTitle = parsedScrapeMinDate && isValid(parsedScrapeMinDate) ? format(parsedScrapeMinDate, CONST.DATE.FNS_FORMAT_STRING) : '';

return (
<>
<OfflineWithFeedback
Expand Down Expand Up @@ -107,7 +112,7 @@ function PersonalCardDetailsHeaderMenu({
>
<MenuItemWithTopDescription
description={translate('workspace.moreFeatures.companyCards.transactionStartDate')}
title={card?.scrapeMinDate ? format(parseISO(card.scrapeMinDate), CONST.DATE.FNS_FORMAT_STRING) : ''}
title={transactionStartDateTitle}
shouldShowRightIcon
brickRoadIndicator={card?.errorFields?.scrapeMinDate ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined}
onPress={() => Navigation.navigate(ROUTES.SETTINGS_WALLET_PERSONAL_CARD_EDIT_TRANSACTION_START_DATE.getRoute(cardID))}
Expand Down
41 changes: 41 additions & 0 deletions tests/unit/CardUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4678,6 +4678,7 @@ describe('getCardConnectionStatusDisplay', () => {
actionKey: undefined,
shouldUsePersonalCardFix: false,
shouldUseCompanyCardsLink: false,
shouldUseReauthMessage: false,
});
});

Expand All @@ -4689,6 +4690,7 @@ describe('getCardConnectionStatusDisplay', () => {
actionKey: 'common.actionBadge.fix',
shouldUsePersonalCardFix: true,
shouldUseCompanyCardsLink: false,
shouldUseReauthMessage: false,
});
});

Expand All @@ -4700,6 +4702,7 @@ describe('getCardConnectionStatusDisplay', () => {
actionKey: undefined,
shouldUsePersonalCardFix: false,
shouldUseCompanyCardsLink: true,
shouldUseReauthMessage: false,
});
});

Expand All @@ -4711,6 +4714,7 @@ describe('getCardConnectionStatusDisplay', () => {
actionKey: undefined,
shouldUsePersonalCardFix: false,
shouldUseCompanyCardsLink: false,
shouldUseReauthMessage: false,
});
});

Expand All @@ -4722,6 +4726,43 @@ describe('getCardConnectionStatusDisplay', () => {
actionKey: undefined,
shouldUsePersonalCardFix: false,
shouldUseCompanyCardsLink: false,
shouldUseReauthMessage: false,
});
});

it('returns the reconnect bank message for a broken personal card that needs re-auth', () => {
expect(getCardConnectionStatusDisplay({...defaultParams, isCardBroken: true, doesCardNeedReauthentication: true, isPersonalCard: true})).toEqual({
statusKey: 'walletPage.cardStatus.inactive',
statusTone: 'danger',
messageKey: 'walletPage.cardStatus.reconnectBank',
actionKey: 'common.actionBadge.fix',
shouldUsePersonalCardFix: true,
shouldUseCompanyCardsLink: false,
shouldUseReauthMessage: true,
});
});

it('asks the admin to fix a non-admin company card that needs re-auth instead of showing the reconnect bank message', () => {
expect(getCardConnectionStatusDisplay({...defaultParams, isCardBroken: true, doesCardNeedReauthentication: true, policyID: 'ABC123'})).toEqual({
statusKey: 'walletPage.cardStatus.inactive',
statusTone: 'danger',
messageKey: 'walletPage.cardStatus.askAdminToFixConnection',
actionKey: undefined,
shouldUsePersonalCardFix: false,
shouldUseCompanyCardsLink: false,
shouldUseReauthMessage: false,
});
});

it('prefers the company cards link over the reconnect bank message for an admin', () => {
expect(getCardConnectionStatusDisplay({...defaultParams, isCardBroken: true, doesCardNeedReauthentication: true, isAdminForCardPolicy: true, policyID: 'ABC123'})).toEqual({
statusKey: 'walletPage.cardStatus.inactive',
statusTone: 'danger',
messageKey: 'walletPage.cardStatus.fixConnectionIn',
actionKey: undefined,
shouldUsePersonalCardFix: false,
shouldUseCompanyCardsLink: true,
shouldUseReauthMessage: false,
});
});
});
Expand Down
Loading