diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 55eaf64d919f1..671d45d2e2ba1 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1241,6 +1241,7 @@ const CONST = { CARD_REPLACED_VIRTUAL: 'CARDREPLACEDVIRTUAL', CARD_REPLACED: 'CARDREPLACED', CARD_ASSIGNED: 'CARDASSIGNED', + PERSONAL_CARD_CONNECTION_BROKEN: 'PERSONALCARDCONNECTIONBROKEN', CHANGE_FIELD: 'CHANGEFIELD', // OldDot Action CHANGE_POLICY: 'CHANGEPOLICY', CREATED_REPORT_FOR_UNAPPROVED_TRANSACTIONS: 'CREATEDREPORTFORUNAPPROVEDTRANSACTIONS', @@ -5906,6 +5907,7 @@ const CONST = { }, RTER_VIOLATION_TYPES: { BROKEN_CARD_CONNECTION: 'brokenCardConnection', + BROKEN_PERSONAL_CARD_CONNECTION: 'brokenCardConnection', BROKEN_CARD_CONNECTION_530: 'brokenCardConnection530', SEVEN_DAY_HOLD: 'sevenDayHold', }, diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 553a3a7b72f37..16791054323f0 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -309,7 +309,12 @@ const ROUTES = { }, SETTINGS_WALLET_PERSONAL_CARD_DETAILS: { route: 'settings/wallet/personal-card/:cardID', - getRoute: (cardID: string) => `settings/wallet/personal-card/${cardID}` as const, + getRoute: (cardID: string | undefined) => { + if (!cardID) { + Log.warn('Invalid cardID is used to build the SETTINGS_WALLET_PERSONAL_CARD_DETAILS route'); + } + return `settings/wallet/personal-card/${cardID}` as const; + }, }, SETTINGS_WALLET_PERSONAL_CARD_EDIT_NAME: { route: 'settings/wallet/personal-card/:cardID/edit/name', diff --git a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx index 9ec42953b9802..e36131cc8bcc0 100644 --- a/src/components/ReportActionItem/MoneyRequestReceiptView.tsx +++ b/src/components/ReportActionItem/MoneyRequestReceiptView.tsx @@ -122,6 +122,7 @@ function MoneyRequestReceiptView({ const [transaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(linkedTransactionID)}`, {canBeMissing: true}); const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${moneyRequestReport?.policyID}`, {canBeMissing: true}); + const [cardList] = useOnyx(ONYXKEYS.CARD_LIST, {canBeMissing: true}); const transactionViolations = useTransactionViolations(transaction?.transactionID); const isDistanceRequest = isDistanceRequestTransactionUtils(transaction); @@ -137,6 +138,7 @@ function MoneyRequestReceiptView({ const isEditable = !!canUserPerformWriteActionReportUtils(report, isReportArchived) && !readonly; const canEdit = isMoneyRequestAction(parentReportAction) && canEditMoneyRequest(parentReportAction, isChatReportArchived, moneyRequestReport, policy, transaction) && isEditable; const companyCardPageURL = `${environmentURL}/${ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(report?.policyID)}`; + const connectionLink = transaction?.cardID ? `${environmentURL}/${ROUTES.SETTINGS_WALLET_PERSONAL_CARD_DETAILS.getRoute(transaction.cardID)}` : undefined; const canEditReceipt = isEditable && canEditFieldOfMoneyRequest(parentReportAction, CONST.EDIT_REQUEST_FIELD.RECEIPT, undefined, isChatReportArchived, undefined, transaction, moneyRequestReport, policy); @@ -173,16 +175,17 @@ function MoneyRequestReceiptView({ for (const violation of filteredViolations) { const isReceiptFieldViolation = receiptFieldViolationNames.has(violation.name); const isReceiptImageViolation = receiptImageViolationNames.has(violation.name); - if (isReceiptFieldViolation || isReceiptImageViolation) { - const violationMessage = ViolationsUtils.getViolationTranslation(violation, translate, canEdit, undefined, companyCardPageURL); + const isRTERViolation = violation.name === CONST.VIOLATIONS.RTER; + if (isReceiptFieldViolation || isReceiptImageViolation || isRTERViolation) { + const violationMessage = ViolationsUtils.getViolationTranslation(violation, translate, canEdit, undefined, companyCardPageURL, connectionLink, cardList); allViolations.push(violationMessage); - if (isReceiptImageViolation) { + if (isReceiptImageViolation || isRTERViolation) { imageViolations.push(violationMessage); } } } return [imageViolations, allViolations]; - }, [transactionViolations, translate, canEdit, companyCardPageURL]); + }, [transactionViolations, translate, canEdit, companyCardPageURL, connectionLink, cardList]); const receiptRequiredViolation = transactionViolations?.some((violation) => violation.name === CONST.VIOLATIONS.RECEIPT_REQUIRED); const itemizedReceiptRequiredViolation = transactionViolations?.some((violation) => violation.name === CONST.VIOLATIONS.ITEMIZED_RECEIPT_REQUIRED); diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 9d8489ed232b0..7de4bba0e7025 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -232,6 +232,7 @@ function MoneyRequestView({ const allPolicyTags = usePolicyTags(); const policyTagList = allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${targetPolicyID}`]; const [nonPersonalAndWorkspaceCards] = useOnyx(ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST, {canBeMissing: true}); + const [cardList] = useOnyx(ONYXKEYS.CARD_LIST, {canBeMissing: true}); const [transactionBackup] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_BACKUP}${getNonEmptyStringOnyxID(linkedTransactionID)}`, {canBeMissing: true}); const transactionViolations = useTransactionViolations(transaction?.transactionID); @@ -327,6 +328,7 @@ function MoneyRequestView({ const isEditable = !!canUserPerformWriteActionReportUtils(transactionThreadReport, isReportArchived) && !readonly; const canEdit = isMoneyRequestAction(parentReportAction) && canEditMoneyRequest(parentReportAction, isChatReportArchived, moneyRequestReport, policy, transaction) && isEditable; const companyCardPageURL = `${environmentURL}/${ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(transactionThreadReport?.policyID)}`; + const connectionLink = transaction?.cardID ? `${environmentURL}/${ROUTES.SETTINGS_WALLET_PERSONAL_CARD_DETAILS.getRoute(transaction.cardID)}` : undefined; const [originalTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(transaction?.comment?.originalTransactionID)}`, {canBeMissing: true}); const {isExpenseSplit} = getOriginalTransactionWithSplitInfo(transaction, originalTransaction); const [transactionReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transaction?.reportID}`, {canBeMissing: true}); @@ -568,7 +570,7 @@ function MoneyRequestView({ // Return violations if there are any if (field !== 'merchant' && hasViolations(field, data, policyHasDependentTags, tagValue)) { const violations = getViolationsForField(field, data, policyHasDependentTags, tagValue); - return `${violations.map((violation) => ViolationsUtils.getViolationTranslation(violation, translate, canEdit, undefined, companyCardPageURL)).join('. ')}.`; + return `${violations.map((violation) => ViolationsUtils.getViolationTranslation(violation, translate, canEdit, undefined, companyCardPageURL, connectionLink, cardList)).join('. ')}.`; } if (field === 'attendees' && isMissingAttendeesViolation) { @@ -1125,6 +1127,7 @@ function MoneyRequestView({ isLast canEdit={canEdit} companyCardPageURL={companyCardPageURL} + connectionLink={connectionLink} /> )} diff --git a/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx b/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx index 4dfc1e70ee99b..5663c25c8afde 100644 --- a/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx +++ b/src/components/ReportActionItem/TransactionPreview/TransactionPreviewContent.tsx @@ -77,6 +77,7 @@ function TransactionPreviewContent({ ); const {amount, comment: requestComment, merchant, tag, category, currency: requestCurrency} = transactionDetails; const [originalTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${getNonEmptyStringOnyxID(transaction?.comment?.originalTransactionID)}`, {canBeMissing: true}); + const [cardList] = useOnyx(ONYXKEYS.CARD_LIST, {canBeMissing: true}); const managerID = report?.managerID ?? reportPreviewAction?.childManagerAccountID ?? CONST.DEFAULT_NUMBER_ID; const ownerAccountID = report?.ownerAccountID ?? reportPreviewAction?.childOwnerAccountID ?? CONST.DEFAULT_NUMBER_ID; @@ -118,7 +119,10 @@ function TransactionPreviewContent({ const isIOUActionType = isMoneyRequestAction(action); const canEdit = isIOUActionType && canEditMoneyRequest(action, isChatReportArchived, report, policy, transaction); const companyCardPageURL = `${environmentURL}/${ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(report?.policyID)}`; - const violationMessage = firstViolation ? ViolationsUtils.getViolationTranslation(firstViolation, translate, canEdit, undefined, companyCardPageURL) : undefined; + const connectionLink = transaction?.cardID ? `${environmentURL}/${ROUTES.SETTINGS_WALLET_PERSONAL_CARD_DETAILS.getRoute(transaction.cardID)}` : undefined; + const violationMessage = firstViolation + ? ViolationsUtils.getViolationTranslation(firstViolation, translate, canEdit, undefined, companyCardPageURL, connectionLink, cardList) + : undefined; const previewText = useMemo( () => diff --git a/src/components/TransactionItemRow/TransactionItemRowRBR.tsx b/src/components/TransactionItemRow/TransactionItemRowRBR.tsx index bbf0d73916d84..a3830c3eccc16 100644 --- a/src/components/TransactionItemRow/TransactionItemRowRBR.tsx +++ b/src/components/TransactionItemRow/TransactionItemRowRBR.tsx @@ -44,7 +44,9 @@ function TransactionItemRowRBR({transaction, violations, report, containerStyles canBeMissing: true, }); const companyCardPageURL = `${environmentURL}/${ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(report?.policyID)}`; + const connectionLink = transaction?.cardID ? `${environmentURL}/${ROUTES.SETTINGS_WALLET_PERSONAL_CARD_DETAILS.getRoute(transaction.cardID)}` : undefined; const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${report?.policyID}`, {canBeMissing: true}); + const [cardList] = useOnyx(ONYXKEYS.CARD_LIST, {canBeMissing: true}); const transactionThreadId = reportActions ? getIOUActionForTransactionID(Object.values(reportActions ?? {}), transaction.transactionID)?.childReportID : undefined; const [transactionThreadActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadId}`, { canBeMissing: true, @@ -58,6 +60,8 @@ function TransactionItemRowRBR({transaction, violations, report, containerStyles Object.values(transactionThreadActions ?? {}), policyTags, companyCardPageURL, + connectionLink, + cardList, ); return ( diff --git a/src/components/ViolationMessages.tsx b/src/components/ViolationMessages.tsx index 70a6d2fa1bfb3..4dc404c628763 100644 --- a/src/components/ViolationMessages.tsx +++ b/src/components/ViolationMessages.tsx @@ -2,8 +2,10 @@ import React, {useMemo} from 'react'; import {View} from 'react-native'; import type {StyleProp, TextStyle, ViewStyle} from 'react-native'; import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; import ViolationsUtils, {filterReceiptViolations} from '@libs/Violations/ViolationsUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; import type {TransactionViolation} from '@src/types/onyx'; import Text from './Text'; @@ -14,17 +16,23 @@ type ViolationMessagesProps = { textStyle?: StyleProp; canEdit: boolean; companyCardPageURL?: string; + connectionLink?: string; }; -export default function ViolationMessages({violations, isLast, containerStyle, textStyle, canEdit, companyCardPageURL}: ViolationMessagesProps) { +export default function ViolationMessages({violations, isLast, containerStyle, textStyle, canEdit, companyCardPageURL, connectionLink}: ViolationMessagesProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); + const [cardList] = useOnyx(ONYXKEYS.CARD_LIST, {canBeMissing: true}); const filteredViolations = useMemo(() => filterReceiptViolations(violations), [violations]); const violationMessages = useMemo( - () => filteredViolations.map((violation) => [violation.name, ViolationsUtils.getViolationTranslation(violation, translate, canEdit, undefined, companyCardPageURL)]), - [canEdit, translate, filteredViolations, companyCardPageURL], + () => + filteredViolations.map((violation) => [ + violation.name, + ViolationsUtils.getViolationTranslation(violation, translate, canEdit, undefined, companyCardPageURL, connectionLink, cardList), + ]), + [canEdit, translate, filteredViolations, companyCardPageURL, connectionLink, cardList], ); return ( diff --git a/src/languages/de.ts b/src/languages/de.ts index a5904b5ce028e..b3d46033325ed 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -22,6 +22,7 @@ import ObjectUtils from '@src/types/utils/ObjectUtils'; import type en from './en'; import type { ChangeFieldParams, + ConciergeBrokenCardConnectionParams, ConnectionNameParams, CreatedReportForUnapprovedTransactionsParams, DelegateRoleParams, @@ -2139,6 +2140,11 @@ const translations: TranslationDeepObject = { password: 'Bitte geben Sie Ihr Expensify-Passwort ein', }, }, + personalCard: { + brokenConnection: 'Ihre Kartenverbindung ist unterbrochen.', + conciergeBrokenConnection: ({cardName, connectionLink}: ConciergeBrokenCardConnectionParams) => + `Die Verbindung zu Ihrer Karte ${cardName} ist unterbrochen. Melden Sie sich bei Ihrem Online-Banking an, um die Karte zu reparieren.`, + }, walletPage: { balance: 'Kontostand', paymentMethodsTitle: 'Zahlungsmethoden', @@ -7537,7 +7543,7 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard }, customRules: ({message}: ViolationsCustomRulesParams) => message, reviewRequired: 'Überprüfung erforderlich', - rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL}: ViolationsRterParams) => { + rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL, connectionLink}: ViolationsRterParams) => { if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530) { return 'Beleg kann aufgrund einer unterbrochenen Bankverbindung nicht automatisch zugeordnet werden'; } @@ -7549,6 +7555,11 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard if (!isTransactionOlderThan7Days) { return isAdmin ? `Bitte ${member} bitten, es als Barzahlung zu markieren, oder warte 7 Tage und versuche es erneut` : 'Ausstehende Zusammenführung mit Kartenumsatz.'; } + if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_PERSONAL_CARD_CONNECTION) { + return isAdmin + ? `Der Beleg kann aufgrund einer fehlerhaften Kartenverbindung nicht automatisch zugeordnet werden. Markieren Sie ihn als Bargeld, um ihn zu ignorieren, oder korrigieren Sie die Kartenverbindung, damit er zum Beleg passt.` + : 'Automatischer Abgleich des Belegs aufgrund einer unterbrochenen Kartenverbindung nicht möglich.'; + } return ''; }, brokenConnection530Error: 'Beleg ausstehend aufgrund einer unterbrochenen Bankverbindung', diff --git a/src/languages/en.ts b/src/languages/en.ts index 78fe93d1e5d07..5ed6b1798ca62 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -10,6 +10,7 @@ import type {OriginalMessageSettlementAccountLocked, PolicyRulesModifiedFields} import ObjectUtils from '@src/types/utils/ObjectUtils'; import type { ChangeFieldParams, + ConciergeBrokenCardConnectionParams, ConnectionNameParams, CreatedReportForUnapprovedTransactionsParams, DelegateRoleParams, @@ -2182,6 +2183,12 @@ const translations = { password: 'Please enter your Expensify password', }, }, + personalCard: { + brokenConnection: 'Your card connection is broken', + fixCard: 'Fix card', + conciergeBrokenConnection: ({cardName, connectionLink}: ConciergeBrokenCardConnectionParams) => + `Your ${cardName} card connection is broken. Log into your bank to fix the card.`, + }, walletPage: { balance: 'Balance', paymentMethodsTitle: 'Payment methods', @@ -7543,10 +7550,15 @@ const translations = { }, customRules: ({message}: ViolationsCustomRulesParams) => message, reviewRequired: 'Review required', - rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL}: ViolationsRterParams) => { + rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL, connectionLink, isPersonalCard}: ViolationsRterParams) => { if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530) { return "Can't auto-match receipt due to broken bank connection"; } + if (isPersonalCard && (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION || brokenBankConnection)) { + return isAdmin + ? `Can't auto-match receipt due to broken card connection. Mark as cash to ignore, or fix the card to match the receipt.` + : "Can't auto-match receipt due to broken card connection."; + } if (brokenBankConnection || rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION) { return isAdmin ? `Bank connection broken. Reconnect to match receipt` diff --git a/src/languages/es.ts b/src/languages/es.ts index 3a3e63299da42..ede6e9b44b1b4 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -4,7 +4,14 @@ import CONST from '@src/CONST'; import type {OriginalMessageSettlementAccountLocked, PolicyRulesModifiedFields} from '@src/types/onyx/OriginalMessage'; import ObjectUtils from '@src/types/utils/ObjectUtils'; import type en from './en'; -import type {CreatedReportForUnapprovedTransactionsParams, PaidElsewhereParams, RoutedDueToDEWParams, SplitDateRangeParams, ViolationsRterParams} from './params'; +import type { + ConciergeBrokenCardConnectionParams, + CreatedReportForUnapprovedTransactionsParams, + PaidElsewhereParams, + RoutedDueToDEWParams, + SplitDateRangeParams, + ViolationsRterParams, +} from './params'; import type {TranslationDeepObject} from './types'; /* eslint-disable max-len */ @@ -1910,6 +1917,12 @@ const translations: TranslationDeepObject = { password: 'Por favor, introduce tu contraseña de Expensify', }, }, + personalCard: { + brokenConnection: 'La conexión de tu tarjeta está rota', + fixCard: 'Reparar tarjeta', + conciergeBrokenConnection: ({cardName, connectionLink}: ConciergeBrokenCardConnectionParams) => + `La conexión de tu tarjeta ${cardName} está interrumpida. Inicia sesión en tu banco para reparar la tarjeta.`, + }, walletPage: { balance: 'Saldo', paymentMethodsTitle: 'Métodos de pago', @@ -7689,7 +7702,7 @@ ${amount} para ${merchant} - ${date}`, }, customRules: ({message}) => message, reviewRequired: 'Revisión requerida', - rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL}: ViolationsRterParams) => { + rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL, connectionLink}: ViolationsRterParams) => { if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530) { return 'No se puede emparejar automáticamente el recibo debido a una conexión bancaria interrumpida.'; } @@ -7703,6 +7716,11 @@ ${amount} para ${merchant} - ${date}`, ? `Pide a ${member} que marque la transacción como efectivo o espera 7 días e inténtalo de nuevo` : 'Esperando a adjuntar automáticamente la transacción de tarjeta de crédito'; } + if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_PERSONAL_CARD_CONNECTION) { + return isAdmin + ? `No se puede vincular automáticamente el recibo debido a una conexión de tarjeta defectuosa. Márquelo como efectivo para ignorarlo o arregle la tarjeta para que coincida con el recibo.` + : 'No se puede hacer coincidir automáticamente el recibo debido a una conexión de tarjeta rota.'; + } return ''; }, brokenConnection530Error: 'Recibo pendiente debido a una conexión bancaria rota', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 55297041b7030..7f2a31071b876 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -22,6 +22,7 @@ import ObjectUtils from '@src/types/utils/ObjectUtils'; import type en from './en'; import type { ChangeFieldParams, + ConciergeBrokenCardConnectionParams, ConnectionNameParams, CreatedReportForUnapprovedTransactionsParams, DelegateRoleParams, @@ -2146,6 +2147,11 @@ const translations: TranslationDeepObject = { password: 'Veuillez saisir votre mot de passe Expensify', }, }, + personalCard: { + brokenConnection: 'La connexion de votre carte est interrompue.', + conciergeBrokenConnection: ({cardName, connectionLink}: ConciergeBrokenCardConnectionParams) => + `La connexion à votre carte ${cardName} est interrompue. Connectez-vous à votre banque pour rétablir la connexion.`, + }, walletPage: { balance: 'Solde', paymentMethodsTitle: 'Modes de paiement', @@ -7549,7 +7555,7 @@ Exigez des informations de dépense comme les reçus et les descriptions, défin }, customRules: ({message}: ViolationsCustomRulesParams) => message, reviewRequired: 'Révision requise', - rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL}: ViolationsRterParams) => { + rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL, connectionLink}: ViolationsRterParams) => { if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530) { return 'Impossible de faire correspondre automatiquement le reçu en raison d’une connexion bancaire défectueuse'; } @@ -7561,6 +7567,11 @@ Exigez des informations de dépense comme les reçus et les descriptions, défin if (!isTransactionOlderThan7Days) { return isAdmin ? `Demandez à ${member} de marquer comme espèce ou attendez 7 jours et réessayez` : 'En attente de fusion avec la transaction par carte.'; } + if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_PERSONAL_CARD_CONNECTION) { + return isAdmin + ? `Impossible de faire correspondre automatiquement le reçu en raison d'une connexion carte défaillante. Marquez-le comme paiement en espèces pour l'ignorer, ou réparez la carte pour faire correspondre le reçu.` + : "Impossible de faire correspondre automatiquement le reçu en raison d'une connexion carte défaillante."; + } return ''; }, brokenConnection530Error: 'Reçu en attente en raison d’une connexion bancaire rompue', diff --git a/src/languages/it.ts b/src/languages/it.ts index 169e7064b93c1..134fac4c600e8 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -22,6 +22,7 @@ import ObjectUtils from '@src/types/utils/ObjectUtils'; import type en from './en'; import type { ChangeFieldParams, + ConciergeBrokenCardConnectionParams, ConnectionNameParams, CreatedReportForUnapprovedTransactionsParams, DelegateRoleParams, @@ -2136,6 +2137,11 @@ const translations: TranslationDeepObject = { password: 'Inserisci la tua password Expensify', }, }, + personalCard: { + brokenConnection: 'La connessione della tua scheda è interrotta', + conciergeBrokenConnection: ({cardName, connectionLink}: ConciergeBrokenCardConnectionParams) => + `La connessione alla tua carta ${cardName} è interrotta. Accedi alla tua banca per riparare la carta.`, + }, walletPage: { balance: 'Saldo', paymentMethodsTitle: 'Metodi di pagamento', @@ -7527,7 +7533,7 @@ Richiedi dettagli di spesa come ricevute e descrizioni, imposta limiti e valori }, customRules: ({message}: ViolationsCustomRulesParams) => message, reviewRequired: 'Revisione richiesta', - rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL}: ViolationsRterParams) => { + rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL, connectionLink}: ViolationsRterParams) => { if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530) { return 'Impossibile abbinare automaticamente la ricevuta a causa di una connessione bancaria interrotta'; } @@ -7539,6 +7545,11 @@ Richiedi dettagli di spesa come ricevute e descrizioni, imposta limiti e valori if (!isTransactionOlderThan7Days) { return isAdmin ? `Chiedi a ${member} di contrassegnarlo come contante oppure attendi 7 giorni e riprova` : 'In attesa dell’unione con la transazione della carta.'; } + if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_PERSONAL_CARD_CONNECTION) { + return isAdmin + ? `Impossibile associare automaticamente la ricevuta a causa di una connessione interrotta con la carta. Contrassegna come contanti per ignorare o correggi la carta per farla corrispondere alla ricevuta.` + : 'Impossibile abbinare automaticamente la ricevuta a causa di una connessione interrotta con la carta.'; + } return ''; }, brokenConnection530Error: 'Ricevuta in sospeso a causa di connessione bancaria interrotta', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index c4669e8b00932..917cece48b3a2 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -22,6 +22,7 @@ import ObjectUtils from '@src/types/utils/ObjectUtils'; import type en from './en'; import type { ChangeFieldParams, + ConciergeBrokenCardConnectionParams, ConnectionNameParams, CreatedReportForUnapprovedTransactionsParams, DelegateRoleParams, @@ -2131,6 +2132,11 @@ const translations: TranslationDeepObject = { password: 'Expensify のパスワードを入力してください', }, }, + personalCard: { + brokenConnection: 'カード接続が切断されました', + conciergeBrokenConnection: ({cardName, connectionLink}: ConciergeBrokenCardConnectionParams) => + `${cardName} カードの接続が切断されました。銀行にログインしてカードを修復してください。`, + }, walletPage: { balance: '残高', paymentMethodsTitle: '支払方法', @@ -7463,7 +7469,7 @@ ${reportName} }, customRules: ({message}: ViolationsCustomRulesParams) => message, reviewRequired: 'レビューが必要', - rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL}: ViolationsRterParams) => { + rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL, connectionLink}: ViolationsRterParams) => { if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530) { return '銀行連携の不具合により、領収書を自動照合できません'; } @@ -7475,6 +7481,11 @@ ${reportName} if (!isTransactionOlderThan7Days) { return isAdmin ? `${member} に現金としてマークするよう依頼するか、7日間待ってからもう一度お試しください` : 'カード取引との統合を待機中です。'; } + if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_PERSONAL_CARD_CONNECTION) { + return isAdmin + ? `カード接続が切断されているため、レシートを自動照合できません。現金としてマークして無視するか、レシートと一致するようにカードを修正してください。` + : 'カード接続が切断されているため、領収書を自動照合できません。'; + } return ''; }, brokenConnection530Error: '銀行接続の不具合により領収書が保留中', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index e6bd121bd966e..234cf90a11c96 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -22,6 +22,7 @@ import ObjectUtils from '@src/types/utils/ObjectUtils'; import type en from './en'; import type { ChangeFieldParams, + ConciergeBrokenCardConnectionParams, ConnectionNameParams, CreatedReportForUnapprovedTransactionsParams, DelegateRoleParams, @@ -2134,6 +2135,11 @@ const translations: TranslationDeepObject = { password: 'Voer uw Expensify-wachtwoord in', }, }, + personalCard: { + brokenConnection: 'De verbinding met uw netwerkkaart is verbroken.', + conciergeBrokenConnection: ({cardName, connectionLink}: ConciergeBrokenCardConnectionParams) => + `De verbinding met uw kaart ${cardName} is verbroken. Log in bij uw bank om de kaart te herstellen.`, + }, walletPage: { balance: 'Saldo', paymentMethodsTitle: 'Betaalmethoden', @@ -7509,7 +7515,7 @@ Vraag verplichte uitgavedetails zoals bonnetjes en beschrijvingen, stel limieten }, customRules: ({message}: ViolationsCustomRulesParams) => message, reviewRequired: 'Beoordeling vereist', - rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL}: ViolationsRterParams) => { + rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL, connectionLink}: ViolationsRterParams) => { if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530) { return 'Kan bon niet automatisch koppelen vanwege verbroken bankverbinding'; } @@ -7521,6 +7527,11 @@ Vraag verplichte uitgavedetails zoals bonnetjes en beschrijvingen, stel limieten if (!isTransactionOlderThan7Days) { return isAdmin ? `Vraag ${member} om het als contant te markeren of wacht 7 dagen en probeer het opnieuw` : 'In afwachting van samenvoeging met kaarttransactie.'; } + if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_PERSONAL_CARD_CONNECTION) { + return isAdmin + ? `Automatische koppeling van bon mislukt vanwege verbroken kaartverbinding. Markeer als contant om te negeren, of corrigeer de kaart zodat deze overeenkomt met de bon.` + : 'Bon kan niet automatisch worden gekoppeld vanwege een onderbroken kaartverbinding.'; + } return ''; }, brokenConnection530Error: 'Bon in behandeling vanwege verbroken bankverbinding', diff --git a/src/languages/params.ts b/src/languages/params.ts index 82296d24a9879..9a83d54134d9d 100644 --- a/src/languages/params.ts +++ b/src/languages/params.ts @@ -227,6 +227,8 @@ type ViolationsRterParams = { member?: string; rterType?: ValueOf; companyCardPageURL?: string; + connectionLink?: string; + isPersonalCard?: boolean; }; type ViolationsTagOutOfPolicyParams = {tagName?: string} | undefined; @@ -626,6 +628,11 @@ type RoutedDueToDEWParams = { to: string; }; +type ConciergeBrokenCardConnectionParams = { + cardName: string; + connectionLink: string; +}; + export type { SettlementAccountReconciliationParams, ToggleImportTitleParams, @@ -691,6 +698,7 @@ export type { RemovedTheRequestParams, MovedFromReportParams, RenamedRoomActionParams, + ConciergeBrokenCardConnectionParams, ReportArchiveReasonsClosedParams, ReportArchiveReasonsMergedParams, ReportPolicyNameParams, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 8953295506bae..dfac7c48e7aa8 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -22,6 +22,7 @@ import ObjectUtils from '@src/types/utils/ObjectUtils'; import type en from './en'; import type { ChangeFieldParams, + ConciergeBrokenCardConnectionParams, ConnectionNameParams, CreatedReportForUnapprovedTransactionsParams, DelegateRoleParams, @@ -2132,6 +2133,11 @@ const translations: TranslationDeepObject = { password: 'Wprowadź swoje hasło do Expensify', }, }, + personalCard: { + brokenConnection: 'Połączenie z Twoją kartą jest zerwane', + conciergeBrokenConnection: ({cardName, connectionLink}: ConciergeBrokenCardConnectionParams) => + `Połączenie z Twoją kartą ${cardName} jest zerwane. Zaloguj się do swojego banku, aby naprawić kartę.`, + }, walletPage: { balance: 'Saldo', paymentMethodsTitle: 'Metody płatności', @@ -7496,7 +7502,7 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i }, customRules: ({message}: ViolationsCustomRulesParams) => message, reviewRequired: 'Wymagana weryfikacja', - rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL}: ViolationsRterParams) => { + rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL, connectionLink}: ViolationsRterParams) => { if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530) { return 'Nie można automatycznie dopasować paragonu z powodu zerwanego połączenia z bankiem'; } @@ -7508,6 +7514,11 @@ Wymagaj szczegółów wydatków, takich jak paragony i opisy, ustawiaj limity i if (!isTransactionOlderThan7Days) { return isAdmin ? `Poproś ${member}, aby oznaczył jako gotówkę lub poczekaj 7 dni i spróbuj ponownie` : 'Oczekiwanie na połączenie z transakcją z karty.'; } + if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_PERSONAL_CARD_CONNECTION) { + return isAdmin + ? `Nie można automatycznie dopasować paragonu z powodu zerwanego połączenia z kartą. Oznacz jako gotówkę, aby zignorować, lub napraw kartę, aby dopasować ją do paragonu.` + : 'Nie można automatycznie dopasować paragonu z powodu uszkodzonego połączenia karty.'; + } return ''; }, brokenConnection530Error: 'Paragon oczekujący z powodu zerwanego połączenia z bankiem', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 74bc594d78d7e..474e0b7e52c11 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -22,6 +22,7 @@ import ObjectUtils from '@src/types/utils/ObjectUtils'; import type en from './en'; import type { ChangeFieldParams, + ConciergeBrokenCardConnectionParams, ConnectionNameParams, CreatedReportForUnapprovedTransactionsParams, DelegateRoleParams, @@ -2131,6 +2132,11 @@ const translations: TranslationDeepObject = { password: 'Insira sua senha do Expensify', }, }, + personalCard: { + brokenConnection: 'A conexão do seu cartão está interrompida', + conciergeBrokenConnection: ({cardName, connectionLink}: ConciergeBrokenCardConnectionParams) => + `A conexão do seu cartão ${cardName} está interrompida. Faça login no seu banco para corrigir o cartão.`, + }, walletPage: { balance: 'Saldo', paymentMethodsTitle: 'Formas de pagamento', @@ -7498,7 +7504,7 @@ Exija detalhes de despesas como recibos e descrições, defina limites e padrõe }, customRules: ({message}: ViolationsCustomRulesParams) => message, reviewRequired: 'Revisão necessária', - rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL}: ViolationsRterParams) => { + rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL, connectionLink}: ViolationsRterParams) => { if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530) { return 'Não é possível corresponder o recibo automaticamente devido a uma conexão bancária com problemas'; } @@ -7510,6 +7516,11 @@ Exija detalhes de despesas como recibos e descrições, defina limites e padrõe if (!isTransactionOlderThan7Days) { return isAdmin ? `Peça para ${member} marcar como dinheiro em espécie ou aguarde 7 dias e tente novamente` : 'Aguardando combinação com a transação do cartão.'; } + if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_PERSONAL_CARD_CONNECTION) { + return isAdmin + ? `Não foi possível associar o recibo automaticamente devido a uma falha na conexão do cartão. Marque como dinheiro para ignorar ou corrija o cartão para que a correspondência com o recibo seja feita.` + : 'Não foi possível associar o recibo automaticamente devido a uma falha na conexão do cartão.'; + } return ''; }, brokenConnection530Error: 'Recibo pendente devido à conexão bancária quebrada', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 41c860b495bc3..73b9675e3bbbe 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -22,6 +22,7 @@ import ObjectUtils from '@src/types/utils/ObjectUtils'; import type en from './en'; import type { ChangeFieldParams, + ConciergeBrokenCardConnectionParams, ConnectionNameParams, CreatedReportForUnapprovedTransactionsParams, DelegateRoleParams, @@ -2103,6 +2104,11 @@ const translations: TranslationDeepObject = { password: '请输入您的 Expensify 密码', }, }, + personalCard: { + brokenConnection: '您的卡连接已断开', + conciergeBrokenConnection: ({cardName, connectionLink}: ConciergeBrokenCardConnectionParams) => + `您的 ${cardName} 卡连接已断开。登录您的银行 以修复卡片。`, + }, walletPage: { balance: '余额', paymentMethodsTitle: '付款方式', @@ -7334,7 +7340,7 @@ ${reportName} }, customRules: ({message}: ViolationsCustomRulesParams) => message, reviewRequired: '需要审核', - rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL}: ViolationsRterParams) => { + rter: ({brokenBankConnection, isAdmin, isTransactionOlderThan7Days, member, rterType, companyCardPageURL, connectionLink}: ViolationsRterParams) => { if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_CARD_CONNECTION_530) { return '由于银行连接中断,无法自动匹配收据'; } @@ -7344,6 +7350,11 @@ ${reportName} if (!isTransactionOlderThan7Days) { return isAdmin ? `请让 ${member} 标记为现金,或等待 7 天后再试` : '正在等待与卡片交易合并。'; } + if (rterType === CONST.RTER_VIOLATION_TYPES.BROKEN_PERSONAL_CARD_CONNECTION) { + return isAdmin + ? `由于卡片连接故障,无法自动匹配收据。请将其标记为现金以忽略,或修复卡片以匹配收据。` + : '由于卡片连接故障,无法自动匹配收据。'; + } return ''; }, brokenConnection530Error: '由于银行连接中断,收据正在等待处理中', diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index f452722fa3724..9c417bc45e964 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -772,6 +772,15 @@ function isCardPendingReplace(card?: Card) { ); } +/** + * Check if card has a broken connection + * + * @param card personal card to check + */ +function isPersonalCardBrokenConnection(card?: Card) { + return card?.lastScrapeResult && !CONST.COMPANY_CARDS.BROKEN_CONNECTION_IGNORED_STATUSES.includes(card?.lastScrapeResult); +} + function isExpensifyCardPendingAction(card?: Card, privatePersonalDetails?: PrivatePersonalDetails): boolean { return ( card?.bank === CONST.EXPENSIFY_CARD.BANK && @@ -934,6 +943,7 @@ export { getBankName, isSelectedFeedExpired, getCompanyFeeds, + isPersonalCardBrokenConnection, isCustomFeed, getBankCardDetailsImage, getSelectedFeed, diff --git a/src/libs/DebugUtils.ts b/src/libs/DebugUtils.ts index 8d7af62b6891c..f980d97416432 100644 --- a/src/libs/DebugUtils.ts +++ b/src/libs/DebugUtils.ts @@ -1320,6 +1320,7 @@ function validateTransactionViolationDraftProperty(key: keyof TransactionViolati field: 'string', prohibitedExpenseRule: 'string', comment: 'string', + cardID: 'number', }); case 'showInReview': return validateBoolean(value); diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index fcd2631b1c870..c01357fbebdf6 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -17,6 +17,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type { Card, + CompanyCardFeed, OnyxInputOrEntry, OriginalMessageIOU, PersonalDetails, @@ -39,7 +40,7 @@ import type ReportAction from '@src/types/onyx/ReportAction'; import type {Message, OldDotReportAction, OriginalMessage, ReportActions} from '@src/types/onyx/ReportAction'; import type ReportActionName from '@src/types/onyx/ReportActionName'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import {isCardPendingActivate} from './CardUtils'; +import {getBankName, isCardPendingActivate, isPersonalCardBrokenConnection} from './CardUtils'; import {getDecodedCategoryName} from './CategoryUtils'; import {convertAmountToDisplayString, convertToDisplayString, convertToShortDisplayString} from './CurrencyUtils'; import DateUtils from './DateUtils'; @@ -403,6 +404,10 @@ function isHoldAction(reportAction: OnyxInputOrEntry): reportActio return isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.HOLD); } +function isCardBrokenConnectionAction(reportAction: OnyxInputOrEntry): reportAction is ReportAction { + return isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.PERSONAL_CARD_CONNECTION_BROKEN); +} + function isReimbursementDirectionInformationRequiredAction( reportAction: OnyxInputOrEntry, ): reportAction is ReportAction { @@ -3797,6 +3802,15 @@ function getCardIssuedMessage({ } } +function getCardConnectionBrokenMessage(reportAction: OnyxEntry, card: Card | undefined, translate: LocaleContextProps['translate'], connectionLink: string) { + if (!isCardBrokenConnectionAction(reportAction) || !isPersonalCardBrokenConnection(card)) { + return ''; + } + const cardName = card?.cardName; + const personalCardName = cardName ?? getBankName(card?.bank as CompanyCardFeed); + return translate('personalCard.conciergeBrokenConnection', {cardName: personalCardName, connectionLink}); +} + function getRoomChangeLogMessage(translate: LocalizedTranslate, reportAction: ReportAction) { if (!isInviteOrRemovedAction(reportAction)) { return ''; @@ -4035,6 +4049,8 @@ export { isHoldAction, isWhisperAction, isSubmittedAction, + isCardBrokenConnectionAction, + getCardConnectionBrokenMessage, isSubmittedAndClosedAction, isDynamicExternalWorkflowSubmitAction, isMarkAsClosedAction, diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts index ae6f496a22b58..d2a207df885f2 100644 --- a/src/libs/Violations/ViolationsUtils.ts +++ b/src/libs/Violations/ViolationsUtils.ts @@ -5,6 +5,7 @@ import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {LocaleContextProps} from '@components/LocaleContextProvider'; import {getIsMissingAttendeesViolation} from '@libs/AttendeeUtils'; +import {isPersonalCard} from '@libs/CardUtils'; import {getDecodedCategoryName, isCategoryMissing} from '@libs/CategoryUtils'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import DateUtils from '@libs/DateUtils'; @@ -17,7 +18,7 @@ import * as TransactionUtils from '@libs/TransactionUtils'; import {hasValidModifiedAmount, isViolationDismissed, shouldShowViolation} from '@libs/TransactionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Policy, PolicyCategories, PolicyTagLists, Report, ReportAction, Transaction, TransactionViolation, ViolationName} from '@src/types/onyx'; +import type {CardList, Policy, PolicyCategories, PolicyTagLists, Report, ReportAction, Transaction, TransactionViolation, ViolationName} from '@src/types/onyx'; import type {Errors} from '@src/types/onyx/OnyxCommon'; import type {ReceiptError, ReceiptErrors} from '@src/types/onyx/Transaction'; import type ViolationFixParams from './types'; @@ -637,7 +638,15 @@ const ViolationsUtils = { * possible values could be either translation keys that resolve to strings or translation keys that resolve to * functions. */ - getViolationTranslation(violation: TransactionViolation, translate: LocaleContextProps['translate'], canEdit = true, tags?: PolicyTagLists, companyCardPageURL?: string): string { + getViolationTranslation( + violation: TransactionViolation, + translate: LocaleContextProps['translate'], + canEdit = true, + tags?: PolicyTagLists, + companyCardPageURL?: string, + connectionLink?: string, + cardList?: CardList, + ): string { const { brokenBankConnection = false, isAdmin = false, @@ -652,6 +661,7 @@ const ViolationsUtils = { taxName, type, rterType, + cardID, message = '', errorIndexes = [], } = violation.data ?? {}; @@ -715,7 +725,12 @@ const ViolationsUtils = { return translate('violations.itemizedReceiptRequired', {formattedLimit}); case 'customRules': return translate('violations.customRules', {message}); - case 'rter': + case 'rter': { + let isPersonalCardViolation = false; + if (cardID !== undefined && cardID !== null && cardList) { + const card = cardList[cardID]; + isPersonalCardViolation = !!isPersonalCard(card); + } return translate('violations.rter', { brokenBankConnection, isAdmin, @@ -723,7 +738,10 @@ const ViolationsUtils = { member, rterType, companyCardPageURL, + connectionLink, + isPersonalCard: isPersonalCardViolation, }); + } case 'smartscanFailed': return translate('violations.smartscanFailed', {canEdit}); case 'someTagLevelsRequired': @@ -772,6 +790,8 @@ const ViolationsUtils = { transactionThreadActions?: ReportAction[], tags?: PolicyTagLists, companyCardPageURL?: string, + connectionLink?: string, + cardList?: CardList, ): string { const errorMessages = extractErrorMessages(transaction?.errors ?? {}, transactionThreadActions?.filter((e) => !!e.errors) ?? [], translate); const filteredViolations = filterReceiptViolations(transactionViolations); @@ -782,7 +802,7 @@ const ViolationsUtils = { // Some violations end with a period already so lets make sure the connected messages have only single period between them // and end with a single dot. ...filteredViolations.map((violation) => { - const message = ViolationsUtils.getViolationTranslation(violation, translate, true, tags, companyCardPageURL); + const message = ViolationsUtils.getViolationTranslation(violation, translate, true, tags, companyCardPageURL, connectionLink, cardList); if (!message) { return; } diff --git a/src/pages/inbox/report/PureReportActionItem.tsx b/src/pages/inbox/report/PureReportActionItem.tsx index 72a9bad07520e..cd619f073f253 100644 --- a/src/pages/inbox/report/PureReportActionItem.tsx +++ b/src/pages/inbox/report/PureReportActionItem.tsx @@ -42,6 +42,7 @@ import UnreadActionIndicator from '@components/UnreadActionIndicator'; import useActivePolicy from '@hooks/useActivePolicy'; import useConfirmModal from '@hooks/useConfirmModal'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useEnvironment from '@hooks/useEnvironment'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import usePreferredPolicy from '@hooks/usePreferredPolicy'; @@ -73,6 +74,7 @@ import { getAddedConnectionMessage, getAutoPayApprovedReportsEnabledMessage, getAutoReimbursementMessage, + getCardConnectionBrokenMessage, getChangedApproverActionMessage, getCompanyAddressUpdateMessage, getCompanyCardConnectionBrokenMessage, @@ -142,6 +144,7 @@ import { isActionableReportMentionWhisper, isActionableTrackExpense, isActionOfType, + isCardBrokenConnectionAction, isCardIssuedAction, isChronosOOOListAction, isConciergeCategoryOptions, @@ -358,6 +361,9 @@ type PureReportActionItemProps = { /** Whether the room is a chronos report */ isChronosReport?: boolean; + /** All cards */ + cardList?: OnyxTypes.CardList; + /** Function to toggle emoji reaction */ toggleEmojiReaction?: ( reportID: string | undefined, @@ -494,6 +500,7 @@ function PureReportActionItem({ iouReportOfLinkedReport, emojiReactions, linkedTransactionRouteError, + cardList, isUserValidated, parentReport, personalDetails, @@ -558,6 +565,7 @@ function PureReportActionItem({ const isOriginalReportArchived = useReportIsArchived(originalReportID); const isHarvestCreatedExpenseReport = isHarvestCreatedExpenseReportUtils(reportNameValuePairsOrigin, reportNameValuePairsOriginalID); const expensifyIcons = useMemoizedLazyExpensifyIcons(['Eye'] as const); + const {environmentURL} = useEnvironment(); const highlightedBackgroundColorIfNeeded = useMemo( // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing @@ -1606,6 +1614,15 @@ function PureReportActionItem({ policyID={report?.policyID} /> ); + } else if (isCardBrokenConnectionAction(action)) { + const cardID = getOriginalMessage(action)?.cardID; + const card = cardID ? cardList?.[cardID] : undefined; + const connectionLink = cardID ? `${environmentURL}/${ROUTES.SETTINGS_WALLET_PERSONAL_CARD_DETAILS.getRoute(String(cardID))}` : ''; + children = ( + + ${getCardConnectionBrokenMessage(action, card, translate, connectionLink)}`} /> + + ); } else if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.EXPORTED_TO_INTEGRATION)) { children = ; } else if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.RECEIPT_SCAN_FAILED)) { @@ -2134,6 +2151,7 @@ export default memo(PureReportActionItem, (prevProps, nextProps) => { deepEqual(prevProps.taskReport, nextProps.taskReport) && prevProps.shouldHighlight === nextProps.shouldHighlight && deepEqual(prevProps.bankAccountList, nextProps.bankAccountList) && + deepEqual(prevProps.cardList, nextProps.cardList) && prevProps.reportNameValuePairsOrigin === nextProps.reportNameValuePairsOrigin && prevProps.reportNameValuePairsOriginalID === nextProps.reportNameValuePairsOriginalID && prevProps.reportMetadata?.pendingExpenseAction === nextProps.reportMetadata?.pendingExpenseAction diff --git a/src/pages/inbox/report/ReportActionItem.tsx b/src/pages/inbox/report/ReportActionItem.tsx index a8dece8f1384a..2715d03185dd8 100644 --- a/src/pages/inbox/report/ReportActionItem.tsx +++ b/src/pages/inbox/report/ReportActionItem.tsx @@ -1,3 +1,4 @@ +import {filterOutPersonalCards} from '@selectors/Card'; import React from 'react'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {useBlockedFromConcierge} from '@components/OnyxListItemProvider'; @@ -107,6 +108,7 @@ function ReportActionItem({ const movedFromReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(action, CONST.REPORT.MOVE_TYPE.FROM)}`]; const movedToReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(action, CONST.REPORT.MOVE_TYPE.TO)}`]; const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; + const [cardList] = useOnyx(ONYXKEYS.CARD_LIST, {selector: filterOutPersonalCards, canBeMissing: true}); const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST, {canBeMissing: true}); const [personalPolicyID] = useOnyx(ONYXKEYS.PERSONAL_POLICY_ID, {canBeMissing: true}); const {policyForMovingExpensesID} = usePolicyForMovingExpenses(); @@ -137,6 +139,7 @@ function ReportActionItem({ draftMessage={draftMessage} iouReport={iouReport} taskReport={taskReport} + cardList={cardList} linkedReport={linkedReport} iouReportOfLinkedReport={iouReportOfLinkedReport} emojiReactions={emojiReactions} diff --git a/src/pages/settings/Wallet/PersonalCardDetailsPage.tsx b/src/pages/settings/Wallet/PersonalCardDetailsPage.tsx index 2fbcac94fe39c..4b35852828a5d 100644 --- a/src/pages/settings/Wallet/PersonalCardDetailsPage.tsx +++ b/src/pages/settings/Wallet/PersonalCardDetailsPage.tsx @@ -1,9 +1,12 @@ import {format} from 'date-fns'; import React, {useState} from 'react'; import {View} from 'react-native'; +import Button from '@components/Button'; import ConfirmModal from '@components/ConfirmModal'; +import FormHelpMessage from '@components/FormHelpMessage'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ImageSVG from '@components/ImageSVG'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import {useCompanyCardFeedIcons} from '@hooks/useCompanyCardIcons'; @@ -13,14 +16,15 @@ import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import useThemeIllustrations from '@hooks/useThemeIllustrations'; import useThemeStyles from '@hooks/useThemeStyles'; -import {getCardFeedIcon, isPersonalCard} from '@libs/CardUtils'; +import {getCardFeedIcon, isCardConnectionBroken, isPersonalCard} from '@libs/CardUtils'; +import {getLatestErrorField} from '@libs/ErrorUtils'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import {getDisplayNameOrDefault} from '@libs/PersonalDetailsUtils'; import Navigation from '@navigation/Navigation'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import variables from '@styles/variables'; -import {syncCard, unassignCard} from '@userActions/Card'; +import {clearCardErrorField, syncCard, unassignCard} from '@userActions/Card'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -48,6 +52,7 @@ function PersonalCardDetailsPage({route}: PersonalCardDetailsPageProps) { const card = cardList?.[cardID]; const cardBank = card?.bank ?? ''; + const isCardBroken = card ? isCardConnectionBroken(card) : false; const cardholder = personalDetails?.[card?.accountID ?? CONST.DEFAULT_NUMBER_ID]; const displayName = getDisplayNameOrDefault(cardholder); const isUserPersonalCard = !!(card && isPersonalCard(card)); @@ -108,6 +113,34 @@ function PersonalCardDetailsPage({route}: PersonalCardDetailsPageProps) { width={variables.cardPreviewWidth} /> + {isCardBroken && ( + { + if (!card) { + return; + } + clearCardErrorField(card.cardID, 'lastScrape'); + }} + > + + +