diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 35250b0b2fee..13bef54bd79d 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -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]; @@ -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, + // 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, diff --git a/src/languages/de.ts b/src/languages/de.ts index 5915d2dd285f..5750073b896c 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -2589,6 +2589,7 @@ const translations: TranslationDeepObject = { fixConnection: 'Bitte beheben Sie diese Verbindung', fixConnectionIn: (companyCardsRoute: string) => `Bitte beheben Sie diese Verbindung in Firmenkarten`, askAdminToFixConnection: 'Bitte bitten Sie eine(n) Admin, diese Verbindung zu reparieren', + reconnectBank: 'Ihre Bankverbindung muss erneut authentifiziert werden', }, bankAccountStatus: { active: 'Aktiv', diff --git a/src/languages/el.ts b/src/languages/el.ts index 283060d9356b..292f0958c261 100644 --- a/src/languages/el.ts +++ b/src/languages/el.ts @@ -2649,6 +2649,7 @@ const translations: TranslationDeepObject = { fixConnection: 'Παρακαλούμε διορθώστε αυτήν τη σύνδεση', fixConnectionIn: (companyCardsRoute: string) => `Παρακαλούμε διορθώστε αυτήν τη σύνδεση στις εταιρικές κάρτες`, askAdminToFixConnection: 'Παρακαλούμε ζητήστε από έναν διαχειριστή να διορθώσει αυτήν τη σύνδεση', + reconnectBank: 'Η σύνδεσή σας με την τράπεζα χρειάζεται επαναπιστοποίηση', }, bankAccountStatus: { active: 'Ενεργό', diff --git a/src/languages/en.ts b/src/languages/en.ts index bdb170de87bd..6120f191b201 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2648,6 +2648,7 @@ const translations = { fixConnection: 'Please fix this connection', fixConnectionIn: (companyCardsRoute: string) => `Please fix this connection in company cards`, askAdminToFixConnection: 'Please ask an admin to fix this connection', + reconnectBank: 'Your bank connection needs to be re-authenticated', }, bankAccountStatus: { active: 'Active', diff --git a/src/languages/es.ts b/src/languages/es.ts index 3a10781fb8a0..2ec9120a3bf0 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2510,6 +2510,7 @@ const translations: TranslationDeepObject = { fixConnection: 'Por favor, corrige esta conexión', fixConnectionIn: (companyCardsRoute) => `Por favor, corrige esta conexión en tarjetas de la empresa`, askAdminToFixConnection: 'Pídele a un administrador que solucione esta conexión', + reconnectBank: 'Tu conexión bancaria necesita volver a autenticarse', }, bankAccountStatus: { active: 'Activo', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index d829c76b773c..e6800499a7fe 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -2594,6 +2594,7 @@ const translations: TranslationDeepObject = { fixConnection: 'Veuillez corriger cette connexion', fixConnectionIn: (companyCardsRoute: string) => `Veuillez corriger cette connexion dans cartes d’entreprise`, askAdminToFixConnection: 'Veuillez demander à un administrateur de corriger cette connexion', + reconnectBank: 'Votre connexion bancaire doit être réauthentifiée', }, bankAccountStatus: { active: 'Actif', diff --git a/src/languages/it.ts b/src/languages/it.ts index 2713b420896c..20569b028711 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -2582,6 +2582,7 @@ const translations: TranslationDeepObject = { fixConnection: 'Sistema questa connessione per favore', fixConnectionIn: (companyCardsRoute: string) => `Correggi questa connessione in carte aziendali`, askAdminToFixConnection: 'Chiedi a un amministratore di correggere questa connessione', + reconnectBank: 'La connessione con la tua banca deve essere nuovamente autenticata', }, bankAccountStatus: { active: 'Attiva', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 48198cdb496b..a7d62aea90b8 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -2559,6 +2559,7 @@ const translations: TranslationDeepObject = { fixConnection: 'この接続を修正してください', fixConnectionIn: (companyCardsRoute: string) => `この接続を会社カードで修正してください`, askAdminToFixConnection: '管理者にこの接続の修正を依頼してください', + reconnectBank: '銀行連携の再認証が必要です', }, bankAccountStatus: { active: 'アクティブ', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 36e6969ca8d5..f5ca835de24c 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -2583,6 +2583,7 @@ const translations: TranslationDeepObject = { fixConnection: 'Repareer deze verbinding alsjeblieft', fixConnectionIn: (companyCardsRoute: string) => `Repareer deze verbinding in bedrijfskaarten`, askAdminToFixConnection: 'Vraag een/beheerder om deze verbinding te herstellen', + reconnectBank: 'Je bankverbinding moet opnieuw worden geverifieerd', }, bankAccountStatus: { active: 'Actief', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 22a85fd0ec9c..d11865dedd21 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -2575,6 +2575,7 @@ const translations: TranslationDeepObject = { fixConnection: 'Napraw to połączenie', fixConnectionIn: (companyCardsRoute: string) => `Napraw to połączenie w firmowe karty`, askAdminToFixConnection: 'Poproś administratora o naprawienie tego połączenia', + reconnectBank: 'Twoje połączenie z bankiem wymaga ponownego uwierzytelnienia', }, bankAccountStatus: { active: 'Aktywne', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 21135aaf716b..00e9f5597ff7 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -2576,6 +2576,7 @@ const translations: TranslationDeepObject = { fixConnection: 'Corrija esta conexão', fixConnectionIn: (companyCardsRoute: string) => `Corrija esta conexão em cartões corporativos`, askAdminToFixConnection: 'Peça para um administrador corrigir essa conexão', + reconnectBank: 'Sua conexão bancária precisa ser autenticada novamente', }, bankAccountStatus: { active: 'Ativo', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 16f25c35af01..5e27d16d58e1 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -2490,6 +2490,7 @@ const translations: TranslationDeepObject = { fixConnection: '请修复此连接', fixConnectionIn: (companyCardsRoute: string) => `请在公司卡中修复此连接`, askAdminToFixConnection: '请联系管理员修复此连接', + reconnectBank: '您的银行连接需要重新验证', }, bankAccountStatus: { active: '活跃', diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index e0cfb4defd9b..f41d8fb9a408 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -128,6 +128,7 @@ type CardConnectionStatusDisplay = { actionKey?: TranslationPaths; shouldUsePersonalCardFix?: boolean; shouldUseCompanyCardsLink?: boolean; + shouldUseReauthMessage?: boolean; }; type CardConnectionStatusDisplayParams = { @@ -137,6 +138,7 @@ type CardConnectionStatusDisplayParams = { isCardInactive: boolean; isPersonalCard: boolean; isAdminForCardPolicy: boolean; + doesCardNeedReauthentication?: boolean; policyID?: string; }; @@ -1369,6 +1371,16 @@ 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, @@ -1376,6 +1388,7 @@ function getCardConnectionStatusDisplay({ isCardInactive: isCardInactiveStatus, isPersonalCard: isPersonalCardStatus, isAdminForCardPolicy, + doesCardNeedReauthentication, policyID, }: CardConnectionStatusDisplayParams): CardConnectionStatusDisplay | undefined { if (!shouldShowConnectionStatus) { @@ -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 { @@ -1404,6 +1420,7 @@ function getCardConnectionStatusDisplay({ actionKey: shouldUsePersonalCardFix ? 'common.actionBadge.fix' : undefined, shouldUsePersonalCardFix, shouldUseCompanyCardsLink, + shouldUseReauthMessage, }; } @@ -2132,6 +2149,7 @@ export { getCSVFeedType, getFeedType, isCardConnectionBroken, + doesCardConnectionNeedReauthentication, getCardConnectionStatusDisplay, isBrokenConnectionPastDismissThreshold, isSmartLimitEnabled, diff --git a/src/pages/settings/Wallet/PaymentMethodList.tsx b/src/pages/settings/Wallet/PaymentMethodList.tsx index b299ab09a588..0389344860b6 100644 --- a/src/pages/settings/Wallet/PaymentMethodList.tsx +++ b/src/pages/settings/Wallet/PaymentMethodList.tsx @@ -24,6 +24,7 @@ import { isActionableVirtualExpensifyCard, isBrokenConnectionPastDismissThreshold, isCardConnectionBroken, + doesCardConnectionNeedReauthentication, isCardFrozen, isCardInactive, isExpensifyCard, @@ -322,6 +323,7 @@ function PaymentMethodList({ isCardInactive: isCardInactiveState, isPersonalCard: isUserPersonalCard, isAdminForCardPolicy, + doesCardNeedReauthentication: doesCardConnectionNeedReauthentication(card), policyID: policyIDForCard, }); const shouldShowCardConnectionMessage = !!cardConnectionStatusDisplay?.messageKey; @@ -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) { diff --git a/src/pages/settings/Wallet/PersonalCardDetailsHeaderMenu.tsx b/src/pages/settings/Wallet/PersonalCardDetailsHeaderMenu.tsx index 74767651a720..585e14068893 100644 --- a/src/pages/settings/Wallet/PersonalCardDetailsHeaderMenu.tsx +++ b/src/pages/settings/Wallet/PersonalCardDetailsHeaderMenu.tsx @@ -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'; @@ -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 ( <> Navigation.navigate(ROUTES.SETTINGS_WALLET_PERSONAL_CARD_EDIT_TRANSACTION_START_DATE.getRoute(cardID))} diff --git a/tests/unit/CardUtilsTest.ts b/tests/unit/CardUtilsTest.ts index 536f11ae7005..cdff29bae665 100644 --- a/tests/unit/CardUtilsTest.ts +++ b/tests/unit/CardUtilsTest.ts @@ -4678,6 +4678,7 @@ describe('getCardConnectionStatusDisplay', () => { actionKey: undefined, shouldUsePersonalCardFix: false, shouldUseCompanyCardsLink: false, + shouldUseReauthMessage: false, }); }); @@ -4689,6 +4690,7 @@ describe('getCardConnectionStatusDisplay', () => { actionKey: 'common.actionBadge.fix', shouldUsePersonalCardFix: true, shouldUseCompanyCardsLink: false, + shouldUseReauthMessage: false, }); }); @@ -4700,6 +4702,7 @@ describe('getCardConnectionStatusDisplay', () => { actionKey: undefined, shouldUsePersonalCardFix: false, shouldUseCompanyCardsLink: true, + shouldUseReauthMessage: false, }); }); @@ -4711,6 +4714,7 @@ describe('getCardConnectionStatusDisplay', () => { actionKey: undefined, shouldUsePersonalCardFix: false, shouldUseCompanyCardsLink: false, + shouldUseReauthMessage: false, }); }); @@ -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, }); }); });