From f17d155738b46ebfeacc46389013095eac9abd12 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Thu, 2 Oct 2025 13:54:22 -1000 Subject: [PATCH 01/14] add fraud alert report action --- src/CONST/index.ts | 1 + .../ReportActionItem/CardFraudAlert.tsx | 78 +++++++++++++++++++ src/libs/ReportActionsUtils.ts | 5 ++ .../home/report/PureReportActionItem.tsx | 52 +++++++++++++ src/types/onyx/OriginalMessage.ts | 22 ++++++ 5 files changed, 158 insertions(+) create mode 100644 src/components/ReportActionItem/CardFraudAlert.tsx diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 3ff29cfbdd202..c82cb11ef57c8 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1168,6 +1168,7 @@ const CONST = { // OldDot Actions render getMessage from Web-Expensify/lib/Report/Action PHP files via getMessageOfOldDotReportAction in ReportActionsUtils.ts TYPE: { ACTIONABLE_ADD_PAYMENT_CARD: 'ACTIONABLEADDPAYMENTCARD', + ACTIONABLE_CARD_FRAUD_ALERT: 'ACTIONABLECARDFRAUDALERT', ACTIONABLE_JOIN_REQUEST: 'ACTIONABLEJOINREQUEST', ACTIONABLE_MENTION_WHISPER: 'ACTIONABLEMENTIONWHISPER', ACTIONABLE_MENTION_INVITE_TO_SUBMIT_EXPENSE_CONFIRM_WHISPER: 'ACTIONABLEMENTIONINVITETOSUBMITEXPENSECONFIRMWHISPER', diff --git a/src/components/ReportActionItem/CardFraudAlert.tsx b/src/components/ReportActionItem/CardFraudAlert.tsx new file mode 100644 index 0000000000000..9aec49bf209c2 --- /dev/null +++ b/src/components/ReportActionItem/CardFraudAlert.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import {View} from 'react-native'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; +import type {OriginalMessageCardFraudAlert} from '@src/types/onyx/OriginalMessage'; +import ActionableItemButtons from './ActionableItemButtons'; + +type CardFraudAlertProps = { + /** The card fraud alert original message */ + originalMessage: OriginalMessageCardFraudAlert; + + /** Callback when user confirms it was them */ + onConfirm: () => void; + + /** Callback when user reports fraud */ + onReportFraud: () => void; +}; + +function CardFraudAlert({originalMessage, onConfirm, onReportFraud}: CardFraudAlertProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const {cardID, maskedCardNumber, triggerAmount, triggerMerchant, resolution} = originalMessage; + const formattedAmount = CurrencyUtils.convertToDisplayString(triggerAmount, 'USD'); + const cardLastFour = maskedCardNumber.slice(-4); + + if (resolution) { + const resolutionMessage = resolution === 'recognized' + ? 'cleared the earlier suspicious activity. The card is reactivated. You\'re all set to keep on expensin\'!' + : 'the card has been deactivated.'; + + return ( + + + {resolutionMessage} + + + ); + } + + return ( + + + I identified suspicious Expensify Card activity for your Expensify Card ending in {cardLastFour}. Do you recognize these charges? + + + + + {formattedAmount} at {triggerMerchant} + + + + + + ); +} + +CardFraudAlert.displayName = 'CardFraudAlert'; + +export default CardFraudAlert; + diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index f0fcfa7feb061..4b5787e7f0e0a 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -2064,6 +2064,10 @@ function isActionableAddPaymentCard(reportAction: OnyxEntry): repo return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_ADD_PAYMENT_CARD; } +function isActionableCardFraudAlert(reportAction: OnyxEntry): reportAction is ReportAction { + return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_CARD_FRAUD_ALERT; +} + function getExportIntegrationLastMessageText(reportAction: OnyxEntry): string { const fragments = getExportIntegrationActionFragments(reportAction); return fragments.reduce((acc, fragment) => `${acc} ${fragment.text}`, ''); @@ -3189,6 +3193,7 @@ export { wasActionTakenByCurrentUser, isInviteOrRemovedAction, isActionableAddPaymentCard, + isActionableCardFraudAlert, getExportIntegrationActionFragments, getExportIntegrationLastMessageText, getExportIntegrationMessageHTML, diff --git a/src/pages/home/report/PureReportActionItem.tsx b/src/pages/home/report/PureReportActionItem.tsx index f1e20920f310d..6470cad588253 100644 --- a/src/pages/home/report/PureReportActionItem.tsx +++ b/src/pages/home/report/PureReportActionItem.tsx @@ -50,6 +50,7 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import ControlSelection from '@libs/ControlSelection'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import type {OnyxDataWithErrors} from '@libs/ErrorUtils'; import {getLatestErrorMessageField, isReceiptError} from '@libs/ErrorUtils'; @@ -105,6 +106,7 @@ import { getWorkspaceTagUpdateMessage, getWorkspaceUpdateFieldMessage, isActionableAddPaymentCard, + isActionableCardFraudAlert, isActionableJoinRequest, isActionableMentionInviteToSubmitExpenseConfirmWhisper, isActionableMentionWhisper, @@ -815,6 +817,32 @@ function PureReportActionItem({ return options; } + if (isActionableCardFraudAlert(action)) { + if (action.originalMessage?.resolution) { + return []; + } + + return [ + { + text: 'Yes', + key: `${action.reportActionID}-cardFraudAlert-confirm`, + onPress: () => { + // TODO: Call API to confirm it was the user + console.log('User confirmed transaction'); + }, + isPrimary: true, + }, + { + text: 'No', + key: `${action.reportActionID}-cardFraudAlert-reportFraud`, + onPress: () => { + // TODO: Call API to report fraud + console.log('User reported fraud'); + }, + }, + ]; + } + if (isActionableJoinRequest(action)) { return [ { @@ -1300,6 +1328,30 @@ function PureReportActionItem({ children = ; } else if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.REMOVED_FROM_APPROVAL_CHAIN)) { children = ; + } else if (isActionableCardFraudAlert(action)) { + const fraudMessage = getOriginalMessage(action); + const cardLastFour = fraudMessage?.maskedCardNumber?.slice(-4) ?? ''; + const formattedAmount = CurrencyUtils.convertToDisplayString(fraudMessage?.triggerAmount ?? 0, 'USD'); + const merchant = fraudMessage?.triggerMerchant ?? ''; + const resolution = fraudMessage?.resolution; + + const message = resolution + ? (resolution === 'recognized' + ? 'cleared the earlier suspicious activity. The card is reactivated. You\'re all set to keep on expensin\'!' + : 'the card has been deactivated.') + : `I identified suspicious Expensify Card activity for your Expensify Card ending in ${cardLastFour}. Do you recognize these charges?\n\n${formattedAmount} at ${merchant}`; + + children = ( + + + {actionableItemButtons.length > 0 && ( + + )} + + ); } else if (isActionableJoinRequest(action)) { children = ( diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index 60fc313817c5b..cae18502c52f5 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -147,6 +147,26 @@ type OriginalMessageActionableMentionWhisper = { whisperedTo?: number[]; }; +type OriginalMessageCardFraudAlert = { + /** Card ID */ + cardID: number; + + /** Masked card number */ + maskedCardNumber: string; + + /** Transaction amount in cents */ + triggerAmount: number; + + /** Merchant name */ + triggerMerchant: string; + + /** Timestamp */ + date: string; + + /** Resolution: 'recognized' or 'fraud' */ + resolution?: string; +}; + /** Model of `actionable mention whisper` report action */ type OriginalMessageActionableMentionInviteToSubmitExpenseConfirmWhisper = { /** Account IDs of users that aren't members of the room */ @@ -959,6 +979,7 @@ type OriginalMessageReimbursementDirectorInformationRequired = { /* eslint-disable jsdoc/require-jsdoc */ type OriginalMessageMap = { [CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_ADD_PAYMENT_CARD]: OriginalMessageAddPaymentCard; + [CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_CARD_FRAUD_ALERT]: OriginalMessageCardFraudAlert; [CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_JOIN_REQUEST]: OriginalMessageJoinPolicy; [CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_MENTION_WHISPER]: OriginalMessageActionableMentionWhisper; [CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_MENTION_INVITE_TO_SUBMIT_EXPENSE_CONFIRM_WHISPER]: OriginalMessageActionableMentionInviteToSubmitExpenseConfirmWhisper; @@ -1055,6 +1076,7 @@ type OriginalMessage = OriginalMessageMap[T]; export default OriginalMessage; export type { DecisionName, + OriginalMessageCardFraudAlert, OriginalMessageIOU, ChronosOOOEvent, PaymentMethodType, From 323466255910a2559a96f80740b5d33bcb120ca4 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Thu, 2 Oct 2025 14:44:42 -1000 Subject: [PATCH 02/14] Make sure this alert can be rendered --- src/libs/ReportActionsUtils.ts | 6 +++--- src/pages/home/report/PureReportActionItem.tsx | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 4b5787e7f0e0a..9bc4710599323 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -149,7 +149,7 @@ function isCreatedAction(reportAction: OnyxInputOrEntry): boolean } function isDeletedAction(reportAction: OnyxInputOrEntry): boolean { - if (isInviteOrRemovedAction(reportAction) || isActionableMentionWhisper(reportAction)) { + if (isInviteOrRemovedAction(reportAction) || isActionableMentionWhisper(reportAction) || isActionableCardFraudAlert(reportAction)) { return false; } @@ -884,7 +884,7 @@ function shouldReportActionBeVisible(reportAction: OnyxEntry, key: } if ( - (isActionableReportMentionWhisper(reportAction) || isActionableJoinRequestPendingReportAction(reportAction) || isActionableMentionWhisper(reportAction)) && + (isActionableReportMentionWhisper(reportAction) || isActionableJoinRequestPendingReportAction(reportAction) || isActionableMentionWhisper(reportAction) || isActionableCardFraudAlert(reportAction)) && !canUserPerformWriteAction ) { return false; @@ -2064,7 +2064,7 @@ function isActionableAddPaymentCard(reportAction: OnyxEntry): repo return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_ADD_PAYMENT_CARD; } -function isActionableCardFraudAlert(reportAction: OnyxEntry): reportAction is ReportAction { +function isActionableCardFraudAlert(reportAction: OnyxInputOrEntry): reportAction is ReportAction { return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_CARD_FRAUD_ALERT; } diff --git a/src/pages/home/report/PureReportActionItem.tsx b/src/pages/home/report/PureReportActionItem.tsx index 6470cad588253..a2ba6fbfc864d 100644 --- a/src/pages/home/report/PureReportActionItem.tsx +++ b/src/pages/home/report/PureReportActionItem.tsx @@ -766,7 +766,7 @@ function PureReportActionItem({ })); } - if (!isActionableWhisper && (!isActionableJoinRequest(action) || getOriginalMessage(action)?.choice !== ('' as JoinWorkspaceResolution))) { + if (!isActionableWhisper && !isActionableCardFraudAlert(action) && (!isActionableJoinRequest(action) || getOriginalMessage(action)?.choice !== ('' as JoinWorkspaceResolution))) { return []; } @@ -818,7 +818,7 @@ function PureReportActionItem({ } if (isActionableCardFraudAlert(action)) { - if (action.originalMessage?.resolution) { + if (getOriginalMessage(action)?.resolution) { return []; } From 3813d08f041b4ccbcd2186200be3ce302fc2de6d Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Thu, 2 Oct 2025 15:16:03 -1000 Subject: [PATCH 03/14] fix copy --- src/components/ReportActionItem/CardFraudAlert.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/CardFraudAlert.tsx b/src/components/ReportActionItem/CardFraudAlert.tsx index 9aec49bf209c2..e188a76b67aa7 100644 --- a/src/components/ReportActionItem/CardFraudAlert.tsx +++ b/src/components/ReportActionItem/CardFraudAlert.tsx @@ -43,7 +43,7 @@ function CardFraudAlert({originalMessage, onConfirm, onReportFraud}: CardFraudAl return ( - I identified suspicious Expensify Card activity for your Expensify Card ending in {cardLastFour}. Do you recognize these charges? + I identified suspicious activity for your Expensify Card ending in {cardLastFour}. Do you recognize these charges? From e3fd4b29230b4af524c3271781957e62e3ae33e0 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Thu, 2 Oct 2025 15:36:23 -1000 Subject: [PATCH 04/14] Use created time --- .../ReportActionItem/CardFraudAlert.tsx | 78 ------------------- .../home/report/PureReportActionItem.tsx | 6 +- src/types/onyx/OriginalMessage.ts | 3 - 3 files changed, 4 insertions(+), 83 deletions(-) delete mode 100644 src/components/ReportActionItem/CardFraudAlert.tsx diff --git a/src/components/ReportActionItem/CardFraudAlert.tsx b/src/components/ReportActionItem/CardFraudAlert.tsx deleted file mode 100644 index e188a76b67aa7..0000000000000 --- a/src/components/ReportActionItem/CardFraudAlert.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import React from 'react'; -import {View} from 'react-native'; -import Text from '@components/Text'; -import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; -import * as CurrencyUtils from '@libs/CurrencyUtils'; -import type {OriginalMessageCardFraudAlert} from '@src/types/onyx/OriginalMessage'; -import ActionableItemButtons from './ActionableItemButtons'; - -type CardFraudAlertProps = { - /** The card fraud alert original message */ - originalMessage: OriginalMessageCardFraudAlert; - - /** Callback when user confirms it was them */ - onConfirm: () => void; - - /** Callback when user reports fraud */ - onReportFraud: () => void; -}; - -function CardFraudAlert({originalMessage, onConfirm, onReportFraud}: CardFraudAlertProps) { - const styles = useThemeStyles(); - const {translate} = useLocalize(); - - const {cardID, maskedCardNumber, triggerAmount, triggerMerchant, resolution} = originalMessage; - const formattedAmount = CurrencyUtils.convertToDisplayString(triggerAmount, 'USD'); - const cardLastFour = maskedCardNumber.slice(-4); - - if (resolution) { - const resolutionMessage = resolution === 'recognized' - ? 'cleared the earlier suspicious activity. The card is reactivated. You\'re all set to keep on expensin\'!' - : 'the card has been deactivated.'; - - return ( - - - {resolutionMessage} - - - ); - } - - return ( - - - I identified suspicious activity for your Expensify Card ending in {cardLastFour}. Do you recognize these charges? - - - - - {formattedAmount} at {triggerMerchant} - - - - - - ); -} - -CardFraudAlert.displayName = 'CardFraudAlert'; - -export default CardFraudAlert; - diff --git a/src/pages/home/report/PureReportActionItem.tsx b/src/pages/home/report/PureReportActionItem.tsx index a2ba6fbfc864d..90886a2e8797e 100644 --- a/src/pages/home/report/PureReportActionItem.tsx +++ b/src/pages/home/report/PureReportActionItem.tsx @@ -51,6 +51,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import ControlSelection from '@libs/ControlSelection'; import * as CurrencyUtils from '@libs/CurrencyUtils'; +import DateUtils from '@libs/DateUtils'; import {canUseTouchScreen} from '@libs/DeviceCapabilities'; import type {OnyxDataWithErrors} from '@libs/ErrorUtils'; import {getLatestErrorMessageField, isReceiptError} from '@libs/ErrorUtils'; @@ -453,7 +454,7 @@ function PureReportActionItem({ currentUserAccountID, }: PureReportActionItemProps) { const actionSheetAwareScrollViewContext = useContext(ActionSheetAwareScrollView.ActionSheetAwareScrollViewContext); - const {translate, formatPhoneNumber, localeCompare, formatTravelDate} = useLocalize(); + const {translate, formatPhoneNumber, localeCompare, formatTravelDate, datetimeToCalendarTime} = useLocalize(); const personalDetail = useCurrentUserPersonalDetails(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const reportID = report?.reportID ?? action?.reportID; @@ -1334,12 +1335,13 @@ function PureReportActionItem({ const formattedAmount = CurrencyUtils.convertToDisplayString(fraudMessage?.triggerAmount ?? 0, 'USD'); const merchant = fraudMessage?.triggerMerchant ?? ''; const resolution = fraudMessage?.resolution; + const formattedDate = action.created ? datetimeToCalendarTime(action.created, false, false) : ''; const message = resolution ? (resolution === 'recognized' ? 'cleared the earlier suspicious activity. The card is reactivated. You\'re all set to keep on expensin\'!' : 'the card has been deactivated.') - : `I identified suspicious Expensify Card activity for your Expensify Card ending in ${cardLastFour}. Do you recognize these charges?\n\n${formattedAmount} at ${merchant}`; + : `I identified suspicious Expensify Card activity for your Expensify Card ending in ${cardLastFour}. Do you recognize these charges?\n\n${formattedDate} for ${formattedAmount} at ${merchant}`; children = ( diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index cae18502c52f5..02d480cca603c 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -160,9 +160,6 @@ type OriginalMessageCardFraudAlert = { /** Merchant name */ triggerMerchant: string; - /** Timestamp */ - date: string; - /** Resolution: 'recognized' or 'fraud' */ resolution?: string; }; From a9be9478df255e5c41f216fa89eee39cf8cfb933 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Thu, 2 Oct 2025 16:03:41 -1000 Subject: [PATCH 05/14] Add API commands and optimistic handling --- src/libs/API/types.ts | 1 + src/libs/actions/Card.ts | 56 +++++++++++++++++++ .../home/report/PureReportActionItem.tsx | 8 +-- 3 files changed, 61 insertions(+), 4 deletions(-) diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index ebd2632b48cdc..97ce9597fe729 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -43,6 +43,7 @@ const WRITE_COMMANDS = { BANK_ACCOUNT_HANDLE_PLAID_ERROR: 'BankAccount_HandlePlaidError', REPORT_VIRTUAL_EXPENSIFY_CARD_FRAUD: 'ReportVirtualExpensifyCardFraud', REQUEST_REPLACEMENT_EXPENSIFY_CARD: 'RequestReplacementExpensifyCard', + RESOLVE_FRAUD_ALERT: 'ResolveFraudAlert', UPDATE_EXPENSIFY_CARD_LIMIT: 'UpdateExpensifyCardLimit', UPDATE_EXPENSIFY_CARD_TITLE: 'UpdateExpensifyCardTitle', UPDATE_EXPENSIFY_CARD_LIMIT_TYPE: 'UpdateExpensifyCardLimitType', diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index 03387fa2af993..635bf713ad6e1 100644 --- a/src/libs/actions/Card.ts +++ b/src/libs/actions/Card.ts @@ -8,6 +8,7 @@ import type { OpenCardDetailsPageParams, ReportVirtualExpensifyCardFraudParams, RequestReplacementExpensifyCardParams, + ResolveFraudAlertParams, RevealExpensifyCardDetailsParams, StartIssueNewCardFlowParams, UpdateExpensifyCardLimitParams, @@ -967,6 +968,60 @@ function queueExpensifyCardForBilling(feedCountry: string, domainAccountID: numb API.write(WRITE_COMMANDS.QUEUE_EXPENSIFY_CARD_FOR_BILLING, parameters); } +function resolveFraudAlert(cardID: number, isFraud: boolean, reportID: string, reportActionID: string) { + const resolution = isFraud ? 'fraud' : 'recognized'; + + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: { + [reportActionID]: { + originalMessage: { + resolution, + }, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + }, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: { + [reportActionID]: { + pendingAction: null, + }, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: { + [reportActionID]: { + originalMessage: { + resolution: null, + }, + pendingAction: null, + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), + }, + }, + }, + ]; + + const parameters: ResolveFraudAlertParams = { + cardID, + isFraud, + }; + + API.write(WRITE_COMMANDS.RESOLVE_FRAUD_ALERT, parameters, {optimisticData, successData, failureData}); +} + export { requestReplacementExpensifyCard, activatePhysicalExpensifyCard, @@ -994,5 +1049,6 @@ export { getCardDefaultName, queueExpensifyCardForBilling, clearIssueNewCardFormData, + resolveFraudAlert, }; export type {ReplacementReason}; diff --git a/src/pages/home/report/PureReportActionItem.tsx b/src/pages/home/report/PureReportActionItem.tsx index 90886a2e8797e..d421ad0182603 100644 --- a/src/pages/home/report/PureReportActionItem.tsx +++ b/src/pages/home/report/PureReportActionItem.tsx @@ -167,6 +167,7 @@ import {ReactionListContext} from '@pages/home/ReportScreenContext'; import AttachmentModalContext from '@pages/media/AttachmentModalScreen/AttachmentModalContext'; import variables from '@styles/variables'; import {openPersonalBankAccountSetupView} from '@userActions/BankAccounts'; +import {resolveFraudAlert} from '@userActions/Card'; import {hideEmojiPicker, isActive} from '@userActions/EmojiPickerAction'; import {acceptJoinRequest, declineJoinRequest} from '@userActions/Policy/Member'; import {expandURLPreview, resolveActionableMentionConfirmWhisper, resolveConciergeCategoryOptions} from '@userActions/Report'; @@ -823,13 +824,13 @@ function PureReportActionItem({ return []; } + const cardID = getOriginalMessage(action)?.cardID ?? 0; return [ { text: 'Yes', key: `${action.reportActionID}-cardFraudAlert-confirm`, onPress: () => { - // TODO: Call API to confirm it was the user - console.log('User confirmed transaction'); + resolveFraudAlert(cardID, false, reportID ?? '', action.reportActionID); }, isPrimary: true, }, @@ -837,8 +838,7 @@ function PureReportActionItem({ text: 'No', key: `${action.reportActionID}-cardFraudAlert-reportFraud`, onPress: () => { - // TODO: Call API to report fraud - console.log('User reported fraud'); + resolveFraudAlert(cardID, true, reportID ?? '', action.reportActionID); }, }, ]; From cb487b05523df2671214122af71da734fa6db839 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Thu, 2 Oct 2025 16:22:21 -1000 Subject: [PATCH 06/14] Fix this copy my god --- src/pages/home/report/PureReportActionItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/PureReportActionItem.tsx b/src/pages/home/report/PureReportActionItem.tsx index d421ad0182603..c9967bda72112 100644 --- a/src/pages/home/report/PureReportActionItem.tsx +++ b/src/pages/home/report/PureReportActionItem.tsx @@ -1341,7 +1341,7 @@ function PureReportActionItem({ ? (resolution === 'recognized' ? 'cleared the earlier suspicious activity. The card is reactivated. You\'re all set to keep on expensin\'!' : 'the card has been deactivated.') - : `I identified suspicious Expensify Card activity for your Expensify Card ending in ${cardLastFour}. Do you recognize these charges?\n\n${formattedDate} for ${formattedAmount} at ${merchant}`; + : `I identified suspicious activity for your Expensify Card ending in ${cardLastFour}. Do you recognize these charges?\n\n${formattedDate} for ${formattedAmount} at ${merchant}`; children = ( From 37f403b4bd594dab94ee6bf271e46e96ccbc5b19 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Fri, 3 Oct 2025 10:05:30 -1000 Subject: [PATCH 07/14] Translations --- src/languages/de.ts | 20 +++++ src/languages/en.ts | 8 ++ src/languages/es.ts | 83 ++++++++++--------- src/languages/fr.ts | 20 +++++ src/languages/it.ts | 19 +++++ src/languages/ja.ts | 23 +++++ src/languages/nl.ts | 20 +++++ src/languages/pl.ts | 20 +++++ src/languages/pt-BR.ts | 19 +++++ src/languages/zh-hans.ts | 23 +++++ .../API/parameters/ResolveFraudAlertParams.ts | 6 ++ .../home/report/PureReportActionItem.tsx | 20 +++-- 12 files changed, 235 insertions(+), 46 deletions(-) create mode 100644 src/libs/API/parameters/ResolveFraudAlertParams.ts diff --git a/src/languages/de.ts b/src/languages/de.ts index d81004be379f4..05d9220d66eae 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -2011,6 +2011,26 @@ const translations = { validateCardTitle: 'Lassen Sie uns sicherstellen, dass Sie es sind', enterMagicCode: ({contactMethod}: EnterMagicCodeParams) => `Bitte geben Sie den magischen Code ein, der an ${contactMethod} gesendet wurde, um Ihre Kartendetails anzuzeigen. Er sollte in ein bis zwei Minuten ankommen.`, + cardFraudAlert: { + confirmButtonText: 'Ja, das tue ich.', + reportFraudButtonText: 'Nein, das war ich nicht.', + clearedMessage: ({cardLastFour}: {cardLastFour: string}) => + `Die verdächtige Aktivität wurde geklärt und die Karte x${cardLastFour} wurde reaktiviert. Alles bereit, um weiter Ausgaben zu erfassen!`, + deactivatedMessage: ({cardLastFour}: {cardLastFour: string}) => `hat die Karte mit den Endziffern ${cardLastFour} deaktiviert`, + alertMessage: ({ + cardLastFour, + amount, + merchant, + date, + }: { + cardLastFour: string; + amount: string; + merchant: string; + date: string; + }) => `verdächtige Aktivität auf der Karte mit den Endziffern ${cardLastFour} festgestellt. Erkennen Sie diese Belastung? + +${amount} für ${merchant} - ${date}`, + }, }, workflowsPage: { workflowTitle: 'Ausgaben', diff --git a/src/languages/en.ts b/src/languages/en.ts index c2bdb54504b2c..d54f1f59e2525 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1985,6 +1985,14 @@ const translations = { cardDetailsLoadingFailure: 'An error occurred while loading the card details. Please check your internet connection and try again.', validateCardTitle: "Let's make sure it's you", enterMagicCode: ({contactMethod}: EnterMagicCodeParams) => `Please enter the magic code sent to ${contactMethod} to view your card details. It should arrive within a minute or two.`, + cardFraudAlert: { + confirmButtonText: 'Yes, I do', + reportFraudButtonText: "No, it wasn't me", + clearedMessage: ({cardLastFour}: {cardLastFour: string}) => `cleared the suspicious activity and reactivated card x${cardLastFour}. All set to keep expensing!`, + deactivatedMessage: ({cardLastFour}: {cardLastFour: string}) => `deactivated the card ending in ${cardLastFour}`, + 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}`, + }, }, workflowsPage: { workflowTitle: 'Spend', diff --git a/src/languages/es.ts b/src/languages/es.ts index 4ad7827964acd..bbe700e329c3c 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -656,7 +656,11 @@ const translations = { }, supportalNoAccess: { title: 'No tan rápido', - descriptionWithCommand: ({command}: {command?: string} = {}) => + descriptionWithCommand: ({ + command, + }: { + command?: string; + } = {}) => `No estás autorizado para realizar esta acción cuando el soporte ha iniciado sesión (comando: ${command ?? ''}). Si crees que Success debería poder realizar esta acción, inicia una conversación en Slack.`, }, lockedAccount: { @@ -989,16 +993,13 @@ const translations = { if (!added && !updated) { return 'No se han añadido ni actualizado miembros.'; } - if (added && updated) { const getPluralSuffix = (count: number) => (count > 1 ? 's' : ''); return `${added} miembro${getPluralSuffix(added)} añadido${getPluralSuffix(added)}, ${updated} miembro${getPluralSuffix(updated)} actualizado${getPluralSuffix(updated)}.`; } - if (updated) { return updated > 1 ? `${updated} miembros han sido actualizados.` : '1 miembro ha sido actualizado.'; } - return added > 1 ? `Se han agregado ${added} miembros` : 'Se ha agregado 1 miembro.'; }, importTagsSuccessfulDescription: ({tags}: ImportTagsSuccessfulDescriptionParams) => (tags > 1 ? `Se han agregado ${tags} etiquetas.` : 'Se ha agregado 1 etiqueta.'), @@ -1069,7 +1070,11 @@ const translations = { amount: 'Importe', taxAmount: 'Importe del impuesto', taxRate: 'Tasa de impuesto', - approve: ({formattedAmount}: {formattedAmount?: string} = {}) => (formattedAmount ? `Aprobar ${formattedAmount}` : 'Aprobar'), + approve: ({ + formattedAmount, + }: { + formattedAmount?: string; + } = {}) => (formattedAmount ? `Aprobar ${formattedAmount}` : 'Aprobar'), approved: 'Aprobado', cash: 'Efectivo', card: 'Tarjeta', @@ -1198,8 +1203,12 @@ const translations = { payElsewhere: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Marcar ${formattedAmount} como pagado` : `Marcar como pagado`), settleInvoicePersonal: ({amount, last4Digits}: BusinessBankAccountParams) => (amount ? `Pagado ${amount} con cuenta personal ${last4Digits}` : `Pagado con cuenta personal`), settleInvoiceBusiness: ({amount, last4Digits}: BusinessBankAccountParams) => (amount ? `Pagado ${amount} con cuenta de empresa ${last4Digits}` : `Pagado con cuenta de empresa`), - payWithPolicy: ({formattedAmount, policyName}: SettleExpensifyCardParams & {policyName: string}) => - formattedAmount ? `Pay ${formattedAmount} via ${policyName}` : `Pay via ${policyName}`, + payWithPolicy: ({ + formattedAmount, + policyName, + }: SettleExpensifyCardParams & { + policyName: string; + }) => (formattedAmount ? `Pay ${formattedAmount} via ${policyName}` : `Pay via ${policyName}`), businessBankAccount: ({amount, last4Digits}: BusinessBankAccountParams) => amount ? `Pagó ${amount} con la cuenta bancaria ${last4Digits}.` : `Pagó con la cuenta bancaria ${last4Digits}`, automaticallyPaidWithBusinessBankAccount: ({amount, last4Digits}: BusinessBankAccountParams) => @@ -1402,7 +1411,6 @@ const translations = { dates: 'Fechas', rates: 'Tasas', submitsTo: ({name}: SubmitsToParams) => `Se envía a ${name}`, - reject: { educationalTitle: '¿Debes retener o rechazar?', educationalText: 'Si no estás listo para aprobar o pagar un gasto, puedes retenerlo o rechazarlo.', @@ -1419,7 +1427,6 @@ const translations = { markedAsResolved: 'marcó el motivo del rechazo como resuelto', }, }, - moveExpenses: () => ({one: 'Mover gasto', other: 'Mover gastos'}), changeApprover: { title: 'Cambiar aprobador', @@ -1976,6 +1983,23 @@ const translations = { validateCardTitle: 'Asegurémonos de que eres tú', enterMagicCode: ({contactMethod}: EnterMagicCodeParams) => `Introduzca el código mágico enviado a ${contactMethod} para ver los datos de su tarjeta. Debería llegar en un par de minutos.`, + cardFraudAlert: { + clearedMessage: "[es] cleared the earlier suspicious activity. The card is reactivated. You're all set to keep on expensin'!", + deactivatedMessage: '[es] the card has been deactivated.', + alertMessage: ({ + cardLastFour, + amount, + merchant, + date, + }: { + cardLastFour: string; + amount: string; + merchant: string; + date: string; + }) => `[es] identified suspicious activity on card ending in ${cardLastFour}. Do you recognize this charge? + +${amount} for ${merchant} - ${date}`, + }, }, workflowsPage: { workflowTitle: 'Gasto', @@ -2382,7 +2406,6 @@ const translations = { addAccountingIntegrationTask: { title: ({integrationName, workspaceAccountingLink}) => `Conéctate${integrationName === CONST.ONBOARDING_ACCOUNTING_MAPPING.other ? '' : ' a'} [${integrationName === CONST.ONBOARDING_ACCOUNTING_MAPPING.other ? 'tu' : ''} ${integrationName}](${workspaceAccountingLink})`, - description: ({integrationName, workspaceAccountingLink}) => `Conéctate ${integrationName === CONST.ONBOARDING_ACCOUNTING_MAPPING.other ? 'tu' : 'a'} ${integrationName} para la clasificación y sincronización automática de gastos, lo que facilita el cierre de fin de mes.\n` + '\n' + @@ -2592,24 +2615,28 @@ const translations = { smsDeliveryFailureMessage: ({login}: OurEmailProviderParams) => `No hemos podido entregar mensajes SMS a ${login}, así que lo hemos suspendido temporalmente. Por favor, intenta validar tu número:`, validationSuccess: '¡Tu número ha sido validado! Haz clic abajo para enviar un nuevo código mágico de inicio de sesión.', - validationFailed: ({timeData}: {timeData?: {days?: number; hours?: number; minutes?: number} | null}) => { + validationFailed: ({ + timeData, + }: { + timeData?: { + days?: number; + hours?: number; + minutes?: number; + } | null; + }) => { if (!timeData) { return 'Por favor, espera un momento antes de intentarlo de nuevo.'; } - const timeParts = []; if (timeData.days) { timeParts.push(`${timeData.days} ${timeData.days === 1 ? 'día' : 'días'}`); } - if (timeData.hours) { timeParts.push(`${timeData.hours} ${timeData.hours === 1 ? 'hora' : 'horas'}`); } - if (timeData.minutes) { timeParts.push(`${timeData.minutes} ${timeData.minutes === 1 ? 'minuto' : 'minutos'}`); } - let timeText = ''; if (timeParts.length === 1) { timeText = timeParts.at(0) ?? ''; @@ -2618,7 +2645,6 @@ const translations = { } else if (timeParts.length === 3) { timeText = `${timeParts.at(0)}, ${timeParts.at(1)}, y ${timeParts.at(2)}`; } - return `¡Un momento! Debes esperar ${timeText} antes de intentar validar tu número nuevamente.`; }, }, @@ -2712,11 +2738,9 @@ const translations = { }, stepCounter: ({step, total, text}: StepCounterParams) => { let result = `Paso ${step}`; - if (total) { result = `${result} de ${total}`; } - if (text) { result = `${result}: ${text}`; } @@ -3663,19 +3687,16 @@ const translations = { [CONST.QUICKBOOKS_DESKTOP_REIMBURSABLE_ACCOUNT_TYPE.VENDOR_BILL]: 'Factura del proveedor', [CONST.QUICKBOOKS_DESKTOP_REIMBURSABLE_ACCOUNT_TYPE.JOURNAL_ENTRY]: 'Asiento contable', [CONST.QUICKBOOKS_DESKTOP_REIMBURSABLE_ACCOUNT_TYPE.CHECK]: 'Cheque', - [`${CONST.QUICKBOOKS_DESKTOP_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CHECK}Description`]: 'Crearemos un cheque desglosado para cada informe de Expensify y lo enviaremos desde la cuenta bancaria a continuación.', [`${CONST.QUICKBOOKS_DESKTOP_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD}Description`]: "Automáticamente relacionaremos el nombre del comerciante de la transacción con tarjeta de crédito con cualquier proveedor correspondiente en QuickBooks. Si no existen proveedores, crearemos un proveedor asociado 'Credit Card Misc.'.", [`${CONST.QUICKBOOKS_DESKTOP_REIMBURSABLE_ACCOUNT_TYPE.VENDOR_BILL}Description`]: 'Crearemos una factura de proveedor desglosada para cada informe de Expensify con la fecha del último gasto, y la añadiremos a la cuenta a continuación. Si este periodo está cerrado, lo contabilizaremos el 1º del siguiente periodo abierto.', - [`${CONST.QUICKBOOKS_DESKTOP_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD}AccountDescription`]: 'Elige dónde exportar las transacciones con tarjeta de crédito.', [`${CONST.QUICKBOOKS_DESKTOP_REIMBURSABLE_ACCOUNT_TYPE.VENDOR_BILL}AccountDescription`]: 'Selecciona el proveedor que se aplicará a todas las transacciones con tarjeta de crédito.', [`${CONST.QUICKBOOKS_DESKTOP_REIMBURSABLE_ACCOUNT_TYPE.CHECK}AccountDescription`]: 'Elige desde dónde enviar los cheques.', - [`${CONST.QUICKBOOKS_DESKTOP_REIMBURSABLE_ACCOUNT_TYPE.VENDOR_BILL}Error`]: 'Las facturas de proveedores no están disponibles cuando las ubicaciones están habilitadas. Por favor, selecciona otra opción de exportación.', [`${CONST.QUICKBOOKS_DESKTOP_REIMBURSABLE_ACCOUNT_TYPE.CHECK}Error`]: @@ -3780,7 +3801,6 @@ const translations = { outOfPocketTaxEnabledDescription: 'QuickBooks Online no permite impuestos en las exportaciones de entradas a los asientos contables. Como tienes los impuestos activados en tu espacio de trabajo, esta opción de exportación no está disponible.', outOfPocketTaxEnabledError: 'La anotacion en el diario no está disponible cuando los impuestos están activados. Por favor, selecciona otra opción de exportación diferente.', - advancedConfig: { autoSyncDescription: 'Expensify se sincronizará automáticamente con QuickBooks Online todos los días.', inviteEmployees: 'Invitar empleados', @@ -3800,18 +3820,15 @@ const translations = { [CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.VENDOR_BILL]: 'Factura del proveedor', [CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.JOURNAL_ENTRY]: 'Asiento contable', [CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.CHECK]: 'Cheque', - [`${CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.DEBIT_CARD}Description`]: "Automáticamente relacionaremos el nombre del comerciante de la transacción con tarjeta de débito con cualquier proveedor correspondiente en QuickBooks. Si no existen proveedores, crearemos un proveedor asociado 'Debit Card Misc.'.", [`${CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD}Description`]: "Automáticamente relacionaremos el nombre del comerciante de la transacción con tarjeta de crédito con cualquier proveedor correspondiente en QuickBooks. Si no existen proveedores, crearemos un proveedor asociado 'Credit Card Misc.'.", [`${CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.VENDOR_BILL}Description`]: 'Crearemos una factura de proveedor desglosada para cada informe de Expensify con la fecha del último gasto, y la añadiremos a la cuenta a continuación. Si este periodo está cerrado, lo contabilizaremos en el día 1 del siguiente periodo abierto.', - [`${CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.DEBIT_CARD}AccountDescription`]: 'Elige dónde exportar las transacciones con tarjeta de débito.', [`${CONST.QUICKBOOKS_NON_REIMBURSABLE_EXPORT_ACCOUNT_TYPE.CREDIT_CARD}AccountDescription`]: 'Elige dónde exportar las transacciones con tarjeta de crédito.', [`${CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.VENDOR_BILL}AccountDescription`]: 'Selecciona el proveedor que se aplicará a todas las transacciones con tarjeta de crédito.', - [`${CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.VENDOR_BILL}Error`]: 'Las facturas de proveedores no están disponibles cuando las ubicaciones están habilitadas. Por favor, selecciona otra opción de exportación diferente.', [`${CONST.QUICKBOOKS_REIMBURSABLE_ACCOUNT_TYPE.CHECK}Error`]: @@ -3934,7 +3951,6 @@ const translations = { }, }, }, - sageIntacct: { preferredExporter: 'Exportador preferido', taxSolution: 'Solución fiscal', @@ -5094,9 +5110,7 @@ const translations = { return `¿Estás seguro de que quieres desconectar ${integrationName}?`; }, connectPrompt: ({connectionName}: ConnectionNameParams) => - `¿Estás seguro de que quieres conectar a ${ - CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName] ?? 'esta integración contable' - }? Esto eliminará cualquier conexión contable existente.`, + `¿Estás seguro de que quieres conectar a ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName] ?? 'esta integración contable'}? Esto eliminará cualquier conexión contable existente.`, enterCredentials: 'Ingresa tus credenciales', connections: { syncStageName: ({stage}: SyncStageNameConnectionsParams) => { @@ -5297,7 +5311,6 @@ const translations = { deactivateConfirmation: 'Al desactivar esta tarjeta, se rechazarán todas las transacciones futuras y no se podrá deshacer.', }, }, - export: { notReadyHeading: 'No está listo para exportar', notReadyDescription: @@ -5460,7 +5473,6 @@ const translations = { errorTitle: '¡Ups! No tan rapido...', errorDescription: `Hubo un problema al transferir la propiedad de este espacio de trabajo. Inténtalo de nuevo, o contacta con Concierge por ayuda.`, }, - exportAgainModal: { title: '¡Cuidado!', description: ({reportName, connectionName}: ExportAgainModalDescriptionParams) => @@ -5832,7 +5844,6 @@ const translations = { `eliminó a ${approverName} (${approverEmail}) como aprobador para la ${field} "${name}"`, updateApprovalRule: ({field, name, newApproverEmail, newApproverName, oldApproverEmail, oldApproverName}: UpdatedPolicyApprovalRuleParams) => { const formatApprover = (displayName?: string, email?: string) => (displayName ? `${displayName} (${email})` : email); - return `cambió el aprobador para la ${field} "${name}" a ${formatApprover(newApproverName, newApproverEmail)} (previamente ${formatApprover(oldApproverName, oldApproverEmail)})`; }, addCategory: ({categoryName}: UpdatedPolicyCategoryParams) => `añadió la categoría "${categoryName}""`, @@ -5842,7 +5853,6 @@ const translations = { if (!newValue) { return `eliminó la sugerencia de descripción "${oldValue}" de la categoría "${categoryName}"`; } - return !oldValue ? `añadió la sugerencia de descripción "${newValue}" a la categoría "${categoryName}"` : `cambió la sugerencia de descripción de la categoría "${categoryName}" a “${newValue}” (anteriormente “${oldValue}”)`; @@ -5932,9 +5942,7 @@ const translations = { if (toggledOptionsCount && toggledOptionsCount > 1) { return `${allEnabled ? 'habilitó' : 'deshabilitó'} todas las opciones para el campo de informe "${fieldName}"`; } - return `${allEnabled ? 'habilitó' : 'deshabilitó'} la opción "${optionName}" para el campo de informe "${fieldName}", haciendo que todas las opciones queden ${ - allEnabled ? 'habilitadas' : 'deshabilitadas' - }`; + return `${allEnabled ? 'habilitó' : 'deshabilitó'} la opción "${optionName}" para el campo de informe "${fieldName}", haciendo que todas las opciones queden ${allEnabled ? 'habilitadas' : 'deshabilitadas'}`; }, deleteReportField: ({fieldType, fieldName}: AddedOrDeletedPolicyReportFieldParams) => `eliminó el campo de informe ${fieldType} "${fieldName}"`, preventSelfApproval: ({oldValue, newValue}: UpdatedPolicyPreventSelfApprovalParams) => @@ -6369,7 +6377,6 @@ const translations = { if (!newValue) { return `eliminó el campo personalizado 1 de ${email} (previamente "${previousValue}")`; } - return !previousValue ? `añadió "${newValue}" al campo personalizado 1 de ${email}` : `cambió el campo personalizado 1 de ${email} a "${newValue}" (previamente "${previousValue}")`; @@ -6378,7 +6385,6 @@ const translations = { if (!newValue) { return `eliminó el campo personalizado 2 de ${email} (previamente "${previousValue}")`; } - return !previousValue ? `añadió "${newValue}" al campo personalizado 2 de ${email}` : `cambió el campo personalizado 2 de ${email} a "${newValue}" (previamente "${previousValue}")`; @@ -7694,5 +7700,4 @@ const translations = { conciergeWillSend: 'Concierge te enviará el archivo en breve.', }, }; - export default translations satisfies TranslationDeepObject; diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 64301d491a172..31d9413d0039a 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -2010,6 +2010,26 @@ const translations = { validateCardTitle: "Assurons-nous que c'est bien vous", enterMagicCode: ({contactMethod}: EnterMagicCodeParams) => `Veuillez entrer le code magique envoyé à ${contactMethod} pour voir les détails de votre carte. Il devrait arriver dans une minute ou deux.`, + cardFraudAlert: { + confirmButtonText: 'Oui, je le fais.', + reportFraudButtonText: "Non, ce n'était pas moi.", + clearedMessage: ({cardLastFour}: {cardLastFour: string}) => + `a effacé l'activité suspecte et réactivé la carte x${cardLastFour}. Tout est prêt pour continuer à faire des dépenses !`, + deactivatedMessage: ({cardLastFour}: {cardLastFour: string}) => `désactivé la carte se terminant par ${cardLastFour}`, + alertMessage: ({ + cardLastFour, + amount, + merchant, + date, + }: { + cardLastFour: string; + amount: string; + merchant: string; + date: string; + }) => `activité suspecte identifiée sur la carte se terminant par ${cardLastFour}. Reconnaissez-vous cette charge ? + +${amount} pour ${merchant} - ${date}`, + }, }, workflowsPage: { workflowTitle: 'Dépenser', diff --git a/src/languages/it.ts b/src/languages/it.ts index cc7f79141ad28..98a3770a289e1 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -2002,6 +2002,25 @@ const translations = { validateCardTitle: 'Verifichiamo che sei tu', enterMagicCode: ({contactMethod}: EnterMagicCodeParams) => `Inserisci il codice magico inviato a ${contactMethod} per visualizzare i dettagli della tua carta. Dovrebbe arrivare entro un minuto o due.`, + cardFraudAlert: { + confirmButtonText: 'Sì, lo faccio', + reportFraudButtonText: 'No, non ero io', + clearedMessage: ({cardLastFour}: {cardLastFour: string}) => `ho eliminato l'attività sospetta e riattivato la carta x${cardLastFour}. Tutto pronto per continuare a fare spese!`, + deactivatedMessage: ({cardLastFour}: {cardLastFour: string}) => `disattivato la carta che termina con ${cardLastFour}`, + alertMessage: ({ + cardLastFour, + amount, + merchant, + date, + }: { + cardLastFour: string; + amount: string; + merchant: string; + date: string; + }) => `attività sospetta identificata sulla carta che termina con ${cardLastFour}. Riconosci questo addebito? + +${amount} per ${merchant} - ${date}`, + }, }, workflowsPage: { workflowTitle: 'Spendere', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index f26d5cb128779..08b7ec202c89c 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -1995,6 +1995,29 @@ const translations = { cardDetailsLoadingFailure: 'カードの詳細を読み込む際にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。', validateCardTitle: 'あなたであることを確認しましょう', enterMagicCode: ({contactMethod}: EnterMagicCodeParams) => `カードの詳細を表示するには、${contactMethod} に送信されたマジックコードを入力してください。1~2分以内に届くはずです。`, + cardFraudAlert: { + confirmButtonText: 'はい、そうです', + reportFraudButtonText: 'いいえ、それは私ではありませんでした。', + clearedMessage: ({cardLastFour}: {cardLastFour: string}) => + `不審な活動をクリアし、カード x${ + //_/\__/_/ \_,_/\__/\__/\_,_/ + cardLastFour + } を再アクティブ化しました。経費精算を続ける準備が整いました!`, + deactivatedMessage: ({cardLastFour}: {cardLastFour: string}) => `${cardLastFour}で終わるカードを無効化しました`, + alertMessage: ({ + cardLastFour, + amount, + merchant, + date, + }: { + cardLastFour: string; + amount: string; + merchant: string; + date: string; + }) => `カードの末尾が${cardLastFour}のカードで不審な活動が確認されました。この請求を認識していますか? + +${date} - ${merchant}に対する${amount}`, + }, }, workflowsPage: { workflowTitle: '支出', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 6d10f5d8008d5..891da763cdea6 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -2001,6 +2001,26 @@ const translations = { validateCardTitle: 'Laten we ervoor zorgen dat jij het bent', enterMagicCode: ({contactMethod}: EnterMagicCodeParams) => `Voer de magische code in die naar ${contactMethod} is gestuurd om uw kaartgegevens te bekijken. Het zou binnen een minuut of twee moeten aankomen.`, + cardFraudAlert: { + confirmButtonText: 'Ja, dat doe ik.', + reportFraudButtonText: 'Nee, ik was het niet.', + clearedMessage: ({cardLastFour}: {cardLastFour: string}) => + `heeft de verdachte activiteit gewist en kaart x${cardLastFour} opnieuw geactiveerd. Alles klaar om door te gaan met uitgaven!`, + deactivatedMessage: ({cardLastFour}: {cardLastFour: string}) => `deactiveerde de kaart eindigend op ${cardLastFour}`, + alertMessage: ({ + cardLastFour, + amount, + merchant, + date, + }: { + cardLastFour: string; + amount: string; + merchant: string; + date: string; + }) => `verdachte activiteit geïdentificeerd op kaart eindigend op ${cardLastFour}. Herken je deze transactie? + +${amount} voor ${merchant} - ${date}`, + }, }, workflowsPage: { workflowTitle: 'Uitgaven', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 54b718aaa51d6..e2ee716433771 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -1998,6 +1998,26 @@ const translations = { validateCardTitle: 'Upewnijmy się, że to Ty', enterMagicCode: ({contactMethod}: EnterMagicCodeParams) => `Proszę wprowadzić magiczny kod wysłany na ${contactMethod}, aby zobaczyć szczegóły swojej karty. Powinien dotrzeć w ciągu minuty lub dwóch.`, + cardFraudAlert: { + confirmButtonText: 'Tak, robię', + reportFraudButtonText: 'Nie, to nie byłem ja.', + clearedMessage: ({cardLastFour}: {cardLastFour: string}) => + `usunięto podejrzaną aktywność i ponownie aktywowano kartę x${cardLastFour}. Wszystko gotowe do dalszego rozliczania wydatków!`, + deactivatedMessage: ({cardLastFour}: {cardLastFour: string}) => `dezaktywował kartę kończącą się na ${cardLastFour}`, + alertMessage: ({ + cardLastFour, + amount, + merchant, + date, + }: { + cardLastFour: string; + amount: string; + merchant: string; + date: string; + }) => `zidentyfikowano podejrzaną aktywność na karcie kończącej się na ${cardLastFour}. Czy rozpoznajesz tę transakcję? + +${amount} dla ${merchant} - ${date}`, + }, }, workflowsPage: { workflowTitle: 'Wydatki', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 5070a8d094334..da2bdbb2f7c73 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -2001,6 +2001,25 @@ const translations = { validateCardTitle: 'Vamos garantir que é você', enterMagicCode: ({contactMethod}: EnterMagicCodeParams) => `Por favor, insira o código mágico enviado para ${contactMethod} para visualizar os detalhes do seu cartão. Ele deve chegar dentro de um ou dois minutos.`, + cardFraudAlert: { + confirmButtonText: 'Sim, eu aceito', + reportFraudButtonText: 'Não, não fui eu', + clearedMessage: ({cardLastFour}: {cardLastFour: string}) => `limpou a atividade suspeita e reativou o cartão x${cardLastFour}. Tudo pronto para continuar gastando!`, + deactivatedMessage: ({cardLastFour}: {cardLastFour: string}) => `desativou o cartão com final ${cardLastFour}`, + alertMessage: ({ + cardLastFour, + amount, + merchant, + date, + }: { + cardLastFour: string; + amount: string; + merchant: string; + date: string; + }) => `atividade suspeita identificada no cartão terminando em ${cardLastFour}. Você reconhece esta cobrança? + +${amount} para ${merchant} - ${date}`, + }, }, workflowsPage: { workflowTitle: 'Gastar', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 4bcebb156e70b..63e7b8d1af8f7 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -1974,6 +1974,29 @@ const translations = { cardDetailsLoadingFailure: '加载卡片详情时发生错误。请检查您的互联网连接并重试。', validateCardTitle: '让我们确认一下身份', enterMagicCode: ({contactMethod}: EnterMagicCodeParams) => `请输入发送到${contactMethod}的验证码以查看您的卡详细信息。验证码应在一两分钟内到达。`, + cardFraudAlert: { + confirmButtonText: '是的,我愿意', + reportFraudButtonText: '不,不是我', + clearedMessage: ({cardLastFour}: {cardLastFour: string}) => + `已清除可疑活动并重新激活卡片 x${ + //_/\__/_/ \_,_/\__/\__/\_,_/ + cardLastFour + }。一切准备就绪,可以继续报销了!`, + deactivatedMessage: ({cardLastFour}: {cardLastFour: string}) => `已停用以${cardLastFour}结尾的卡片`, + alertMessage: ({ + cardLastFour, + amount, + merchant, + date, + }: { + cardLastFour: string; + amount: string; + merchant: string; + date: string; + }) => `在卡号以${cardLastFour}结尾的卡上发现可疑活动。您是否认可此笔费用? + +${date},${merchant},金额为${amount}。`, + }, }, workflowsPage: { workflowTitle: '花费', diff --git a/src/libs/API/parameters/ResolveFraudAlertParams.ts b/src/libs/API/parameters/ResolveFraudAlertParams.ts new file mode 100644 index 0000000000000..9f639634845e2 --- /dev/null +++ b/src/libs/API/parameters/ResolveFraudAlertParams.ts @@ -0,0 +1,6 @@ +type ResolveFraudAlertParams = { + cardID: number; + isFraud: boolean; +}; + +export default ResolveFraudAlertParams; diff --git a/src/pages/home/report/PureReportActionItem.tsx b/src/pages/home/report/PureReportActionItem.tsx index c9967bda72112..c3f3d301dd7ba 100644 --- a/src/pages/home/report/PureReportActionItem.tsx +++ b/src/pages/home/report/PureReportActionItem.tsx @@ -1,3 +1,4 @@ +import {format} from 'date-fns'; import {deepEqual} from 'fast-equals'; import mapValues from 'lodash/mapValues'; import React, {memo, use, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react'; @@ -455,7 +456,7 @@ function PureReportActionItem({ currentUserAccountID, }: PureReportActionItemProps) { const actionSheetAwareScrollViewContext = useContext(ActionSheetAwareScrollView.ActionSheetAwareScrollViewContext); - const {translate, formatPhoneNumber, localeCompare, formatTravelDate, datetimeToCalendarTime} = useLocalize(); + const {translate, formatPhoneNumber, localeCompare, formatTravelDate, getLocalDateFromDatetime} = useLocalize(); const personalDetail = useCurrentUserPersonalDetails(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const reportID = report?.reportID ?? action?.reportID; @@ -827,7 +828,7 @@ function PureReportActionItem({ const cardID = getOriginalMessage(action)?.cardID ?? 0; return [ { - text: 'Yes', + text: translate('cardPage.cardFraudAlert.confirmButtonText'), key: `${action.reportActionID}-cardFraudAlert-confirm`, onPress: () => { resolveFraudAlert(cardID, false, reportID ?? '', action.reportActionID); @@ -835,7 +836,7 @@ function PureReportActionItem({ isPrimary: true, }, { - text: 'No', + text: translate('cardPage.cardFraudAlert.reportFraudButtonText'), key: `${action.reportActionID}-cardFraudAlert-reportFraud`, onPress: () => { resolveFraudAlert(cardID, true, reportID ?? '', action.reportActionID); @@ -1335,13 +1336,18 @@ function PureReportActionItem({ const formattedAmount = CurrencyUtils.convertToDisplayString(fraudMessage?.triggerAmount ?? 0, 'USD'); const merchant = fraudMessage?.triggerMerchant ?? ''; const resolution = fraudMessage?.resolution; - const formattedDate = action.created ? datetimeToCalendarTime(action.created, false, false) : ''; + const formattedDate = action.created ? format(getLocalDateFromDatetime(action.created), 'MMM do - h:mma') : ''; const message = resolution ? (resolution === 'recognized' - ? 'cleared the earlier suspicious activity. The card is reactivated. You\'re all set to keep on expensin\'!' - : 'the card has been deactivated.') - : `I identified suspicious activity for your Expensify Card ending in ${cardLastFour}. Do you recognize these charges?\n\n${formattedDate} for ${formattedAmount} at ${merchant}`; + ? translate('cardPage.cardFraudAlert.clearedMessage', {cardLastFour}) + : translate('cardPage.cardFraudAlert.deactivatedMessage', {cardLastFour})) + : translate('cardPage.cardFraudAlert.alertMessage', { + cardLastFour, + amount: formattedAmount, + merchant, + date: formattedDate, + }); children = ( From b9a79c6d1b8fdff33162f17e85d327a187f2e8d8 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Fri, 3 Oct 2025 15:00:53 -1000 Subject: [PATCH 08/14] Update date formatting --- src/pages/home/report/PureReportActionItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/PureReportActionItem.tsx b/src/pages/home/report/PureReportActionItem.tsx index c3f3d301dd7ba..19dacecb60bc9 100644 --- a/src/pages/home/report/PureReportActionItem.tsx +++ b/src/pages/home/report/PureReportActionItem.tsx @@ -1336,7 +1336,7 @@ function PureReportActionItem({ const formattedAmount = CurrencyUtils.convertToDisplayString(fraudMessage?.triggerAmount ?? 0, 'USD'); const merchant = fraudMessage?.triggerMerchant ?? ''; const resolution = fraudMessage?.resolution; - const formattedDate = action.created ? format(getLocalDateFromDatetime(action.created), 'MMM do - h:mma') : ''; + const formattedDate = action.created ? format(getLocalDateFromDatetime(action.created), 'MMM. d - h:mma').replace(/am|pm/i, (match) => match.toUpperCase()) : ''; const message = resolution ? (resolution === 'recognized' From fe3e2313966ed835a6c0cd9d753ea92c4791582f Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Wed, 8 Oct 2025 13:59:32 -1000 Subject: [PATCH 09/14] Make sure GBR shows up for this actionable action --- src/CONST/index.ts | 1 + src/libs/ReportUtils.ts | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index c6e6d485ed3b3..6195d7cb776a7 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -7089,6 +7089,7 @@ const CONST = { IS_WAITING_FOR_ASSIGNEE_TO_COMPLETE_ACTION: 'isWaitingForAssigneeToCompleteAction', HAS_CHILD_REPORT_AWAITING_ACTION: 'hasChildReportAwaitingAction', HAS_MISSING_INVOICE_BANK_ACCOUNT: 'hasMissingInvoiceBankAccount', + HAS_UNRESOLVED_CARD_FRAUD_ALERT: 'hasUnresolvedCardFraudAlert', }, RBR_REASONS: { diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 4e913b129e012..119b28b4bc71a 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -191,6 +191,7 @@ import { getWorkspaceReportFieldUpdateMessage, getWorkspaceTagUpdateMessage, getWorkspaceUpdateFieldMessage, + isActionableCardFraudAlert, isActionableJoinRequest, isActionableJoinRequestPending, isActionableTrackExpense, @@ -3822,6 +3823,20 @@ type ReasonAndReportActionThatRequiresAttention = { reportAction?: OnyxEntry; }; +function getUnresolvedCardFraudAlertAction(reportID: string): OnyxEntry { + const reportActions = getAllReportActions(reportID); + return Object.values(reportActions).find( + (action): action is ReportAction => isActionableCardFraudAlert(action) && !getOriginalMessage(action)?.resolution + ); +} + +function hasUnresolvedCardFraudAlert(reportOrOption: OnyxEntry | OptionData): boolean { + if (!reportOrOption?.reportID) { + return false; + } + return !!getUnresolvedCardFraudAlertAction(reportOrOption.reportID); +} + function getReasonAndReportActionThatRequiresAttention( optionOrReport: OnyxEntry | OptionData, parentReportAction?: OnyxEntry, @@ -3840,6 +3855,13 @@ function getReasonAndReportActionThatRequiresAttention( }; } + if (hasUnresolvedCardFraudAlert(optionOrReport)) { + return { + reason: CONST.REQUIRES_ATTENTION_REASONS.HAS_UNRESOLVED_CARD_FRAUD_ALERT, + reportAction: getUnresolvedCardFraudAlertAction(optionOrReport.reportID), + }; + } + if (isReportArchived) { return null; } @@ -12292,6 +12314,8 @@ export { excludeParticipantsForDisplay, getReportName, doesReportContainRequestsFromMultipleUsers, + hasUnresolvedCardFraudAlert, + getUnresolvedCardFraudAlertAction, }; export type { Ancestor, From a868b1cbee643f97d969485128578b20fd4fdd36 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Wed, 8 Oct 2025 14:18:42 -1000 Subject: [PATCH 10/14] Fix style issues --- src/CONST/index.ts | 5 ++++ src/libs/actions/Card.ts | 2 +- .../home/report/PureReportActionItem.tsx | 30 ++++++++++++------- src/types/onyx/OriginalMessage.ts | 3 +- 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 6195d7cb776a7..16ac37e3958bb 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -7092,6 +7092,11 @@ const CONST = { HAS_UNRESOLVED_CARD_FRAUD_ALERT: 'hasUnresolvedCardFraudAlert', }, + CARD_FRAUD_ALERT_RESOLUTION: { + RECOGNIZED: 'recognized', + FRAUD: 'fraud', + }, + RBR_REASONS: { HAS_ERRORS: 'hasErrors', HAS_VIOLATIONS: 'hasViolations', diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index 635bf713ad6e1..be2c72b56967a 100644 --- a/src/libs/actions/Card.ts +++ b/src/libs/actions/Card.ts @@ -969,7 +969,7 @@ function queueExpensifyCardForBilling(feedCountry: string, domainAccountID: numb } function resolveFraudAlert(cardID: number, isFraud: boolean, reportID: string, reportActionID: string) { - const resolution = isFraud ? 'fraud' : 'recognized'; + const resolution = isFraud ? CONST.CARD_FRAUD_ALERT_RESOLUTION.FRAUD : CONST.CARD_FRAUD_ALERT_RESOLUTION.RECOGNIZED; const optimisticData: OnyxUpdate[] = [ { diff --git a/src/pages/home/report/PureReportActionItem.tsx b/src/pages/home/report/PureReportActionItem.tsx index 19dacecb60bc9..777abacece974 100644 --- a/src/pages/home/report/PureReportActionItem.tsx +++ b/src/pages/home/report/PureReportActionItem.tsx @@ -1333,24 +1333,32 @@ function PureReportActionItem({ } else if (isActionableCardFraudAlert(action)) { const fraudMessage = getOriginalMessage(action); const cardLastFour = fraudMessage?.maskedCardNumber?.slice(-4) ?? ''; + + // USD is hardcoded because Expensify cards operate exclusively in USD const formattedAmount = CurrencyUtils.convertToDisplayString(fraudMessage?.triggerAmount ?? 0, 'USD'); const merchant = fraudMessage?.triggerMerchant ?? ''; const resolution = fraudMessage?.resolution; const formattedDate = action.created ? format(getLocalDateFromDatetime(action.created), 'MMM. d - h:mma').replace(/am|pm/i, (match) => match.toUpperCase()) : ''; - const message = resolution - ? (resolution === 'recognized' - ? translate('cardPage.cardFraudAlert.clearedMessage', {cardLastFour}) - : translate('cardPage.cardFraudAlert.deactivatedMessage', {cardLastFour})) - : translate('cardPage.cardFraudAlert.alertMessage', { - cardLastFour, - amount: formattedAmount, - merchant, - date: formattedDate, - }); + let message; + if (!resolution) { + message = translate('cardPage.cardFraudAlert.alertMessage', { + cardLastFour, + amount: formattedAmount, + merchant, + date: formattedDate, + }); + } else if (resolution === CONST.CARD_FRAUD_ALERT_RESOLUTION.RECOGNIZED) { + message = translate('cardPage.cardFraudAlert.clearedMessage', {cardLastFour}); + } else { + message = translate('cardPage.cardFraudAlert.deactivatedMessage', {cardLastFour}); + } children = ( - + {actionableItemButtons.length > 0 && ( ; }; /** Model of `actionable mention whisper` report action */ From 3add22b73c38f524ee0c15fd3ca37504a02387fc Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Wed, 8 Oct 2025 14:21:46 -1000 Subject: [PATCH 11/14] Fix translations --- src/languages/de.ts | 4 ++-- src/languages/fr.ts | 4 ++-- src/languages/it.ts | 3 ++- src/languages/ja.ts | 12 ++++-------- src/languages/nl.ts | 4 ++-- src/languages/pl.ts | 8 ++++---- src/languages/pt-BR.ts | 2 +- src/languages/zh-hans.ts | 9 +++------ 8 files changed, 20 insertions(+), 26 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index 6e299b264a01f..cc222c9272c94 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -2022,7 +2022,7 @@ const translations = { confirmButtonText: 'Ja, das tue ich.', reportFraudButtonText: 'Nein, das war ich nicht.', clearedMessage: ({cardLastFour}: {cardLastFour: string}) => - `Die verdächtige Aktivität wurde geklärt und die Karte x${cardLastFour} wurde reaktiviert. Alles bereit, um weiter Ausgaben zu erfassen!`, + `die verdächtige Aktivität geklärt und die Karte x${cardLastFour} reaktiviert. Alles bereit, um weiter Ausgaben zu erfassen!`, deactivatedMessage: ({cardLastFour}: {cardLastFour: string}) => `hat die Karte mit den Endziffern ${cardLastFour} deaktiviert`, alertMessage: ({ cardLastFour, @@ -2034,7 +2034,7 @@ const translations = { amount: string; merchant: string; date: string; - }) => `verdächtige Aktivität auf der Karte mit den Endziffern ${cardLastFour} festgestellt. Erkennen Sie diese Belastung? + }) => `verdächtige Aktivitäten auf der Karte mit der Endung ${cardLastFour} festgestellt. Erkennen Sie diese Abbuchung? ${amount} für ${merchant} - ${date}`, }, diff --git a/src/languages/fr.ts b/src/languages/fr.ts index d7ef96ec7a1e9..c3870be33829d 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -2019,7 +2019,7 @@ const translations = { enterMagicCode: ({contactMethod}: EnterMagicCodeParams) => `Veuillez entrer le code magique envoyé à ${contactMethod} pour voir les détails de votre carte. Il devrait arriver dans une minute ou deux.`, cardFraudAlert: { - confirmButtonText: 'Oui, je le fais.', + confirmButtonText: 'Oui, je le fais', reportFraudButtonText: "Non, ce n'était pas moi.", clearedMessage: ({cardLastFour}: {cardLastFour: string}) => `a effacé l'activité suspecte et réactivé la carte x${cardLastFour}. Tout est prêt pour continuer à faire des dépenses !`, @@ -2034,7 +2034,7 @@ const translations = { amount: string; merchant: string; date: string; - }) => `activité suspecte identifiée sur la carte se terminant par ${cardLastFour}. Reconnaissez-vous cette charge ? + }) => `activité suspecte identifiée sur la carte se terminant par ${cardLastFour}. Reconnaissez-vous cette transaction ? ${amount} pour ${merchant} - ${date}`, }, diff --git a/src/languages/it.ts b/src/languages/it.ts index 63d7820a9f91e..65aa3389c71d1 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -2012,7 +2012,8 @@ const translations = { cardFraudAlert: { confirmButtonText: 'Sì, lo faccio', reportFraudButtonText: 'No, non ero io', - clearedMessage: ({cardLastFour}: {cardLastFour: string}) => `ho eliminato l'attività sospetta e riattivato la carta x${cardLastFour}. Tutto pronto per continuare a fare spese!`, + clearedMessage: ({cardLastFour}: {cardLastFour: string}) => + `abbiamo eliminato l'attività sospetta e riattivato la carta x${cardLastFour}. Tutto pronto per continuare a registrare le spese!`, deactivatedMessage: ({cardLastFour}: {cardLastFour: string}) => `disattivato la carta che termina con ${cardLastFour}`, alertMessage: ({ cardLastFour, diff --git a/src/languages/ja.ts b/src/languages/ja.ts index a14335732b415..b84721e219bbb 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -2003,14 +2003,10 @@ const translations = { validateCardTitle: 'あなたであることを確認しましょう', enterMagicCode: ({contactMethod}: EnterMagicCodeParams) => `カードの詳細を表示するには、${contactMethod} に送信されたマジックコードを入力してください。1~2分以内に届くはずです。`, cardFraudAlert: { - confirmButtonText: 'はい、そうです', + confirmButtonText: 'はい、そうです。', reportFraudButtonText: 'いいえ、それは私ではありませんでした。', - clearedMessage: ({cardLastFour}: {cardLastFour: string}) => - `不審な活動をクリアし、カード x${ - //_/\__/_/ \_,_/\__/\__/\_,_/ - cardLastFour - } を再アクティブ化しました。経費精算を続ける準備が整いました!`, - deactivatedMessage: ({cardLastFour}: {cardLastFour: string}) => `${cardLastFour}で終わるカードを無効化しました`, + clearedMessage: ({cardLastFour}: {cardLastFour: string}) => `不審な活動をクリアし、カードx${cardLastFour}を再有効化しました。経費精算を続ける準備が整いました!`, + deactivatedMessage: ({cardLastFour}: {cardLastFour: string}) => `${cardLastFour}で終わるカードを無効にしました。`, alertMessage: ({ cardLastFour, amount, @@ -2023,7 +2019,7 @@ const translations = { date: string; }) => `カードの末尾が${cardLastFour}のカードで不審な活動が確認されました。この請求を認識していますか? -${date} - ${merchant}に対する${amount}`, +${date} - ${merchant}に${amount}`, }, }, workflowsPage: { diff --git a/src/languages/nl.ts b/src/languages/nl.ts index abaa405d8c8a7..5e80cde0983f6 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -2010,9 +2010,9 @@ const translations = { `Voer de magische code in die naar ${contactMethod} is gestuurd om uw kaartgegevens te bekijken. Het zou binnen een minuut of twee moeten aankomen.`, cardFraudAlert: { confirmButtonText: 'Ja, dat doe ik.', - reportFraudButtonText: 'Nee, ik was het niet.', + reportFraudButtonText: 'Nee, dat was ik niet.', clearedMessage: ({cardLastFour}: {cardLastFour: string}) => - `heeft de verdachte activiteit gewist en kaart x${cardLastFour} opnieuw geactiveerd. Alles klaar om door te gaan met uitgaven!`, + `verdachte activiteit verwijderd en kaart x${cardLastFour} opnieuw geactiveerd. Alles klaar om door te gaan met declareren!`, deactivatedMessage: ({cardLastFour}: {cardLastFour: string}) => `deactiveerde de kaart eindigend op ${cardLastFour}`, alertMessage: ({ cardLastFour, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index a98bd50fd166e..5f790d2beb870 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -2007,10 +2007,10 @@ const translations = { `Proszę wprowadzić magiczny kod wysłany na ${contactMethod}, aby zobaczyć szczegóły swojej karty. Powinien dotrzeć w ciągu minuty lub dwóch.`, cardFraudAlert: { confirmButtonText: 'Tak, robię', - reportFraudButtonText: 'Nie, to nie byłem ja.', + reportFraudButtonText: 'Nie, to nie byłem ja', clearedMessage: ({cardLastFour}: {cardLastFour: string}) => - `usunięto podejrzaną aktywność i ponownie aktywowano kartę x${cardLastFour}. Wszystko gotowe do dalszego rozliczania wydatków!`, - deactivatedMessage: ({cardLastFour}: {cardLastFour: string}) => `dezaktywował kartę kończącą się na ${cardLastFour}`, + `usunięto podejrzaną aktywność i ponownie aktywowano kartę x${cardLastFour}. Wszystko gotowe do dalszego rozliczania!`, + deactivatedMessage: ({cardLastFour}: {cardLastFour: string}) => `dezaktywowano kartę kończącą się na ${cardLastFour}`, alertMessage: ({ cardLastFour, amount, @@ -2021,7 +2021,7 @@ const translations = { amount: string; merchant: string; date: string; - }) => `zidentyfikowano podejrzaną aktywność na karcie kończącej się na ${cardLastFour}. Czy rozpoznajesz tę transakcję? + }) => `zidentyfikowano podejrzaną aktywność na karcie kończącej się na ${cardLastFour}. Czy rozpoznajesz tę opłatę? ${amount} dla ${merchant} - ${date}`, }, diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 37a098e2bf7f0..2a42cc8d8c1cd 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -2023,7 +2023,7 @@ const translations = { amount: string; merchant: string; date: string; - }) => `atividade suspeita identificada no cartão terminando em ${cardLastFour}. Você reconhece esta cobrança? + }) => `atividade suspeita identificada no cartão com final ${cardLastFour}. Você reconhece esta cobrança? ${amount} para ${merchant} - ${date}`, }, diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index c47020b7ebd99..dfd20d39bbf42 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -1982,13 +1982,10 @@ const translations = { validateCardTitle: '让我们确认一下身份', enterMagicCode: ({contactMethod}: EnterMagicCodeParams) => `请输入发送到${contactMethod}的验证码以查看您的卡详细信息。验证码应在一两分钟内到达。`, cardFraudAlert: { - confirmButtonText: '是的,我愿意', + confirmButtonText: '是的,我愿意。', reportFraudButtonText: '不,不是我', clearedMessage: ({cardLastFour}: {cardLastFour: string}) => - `已清除可疑活动并重新激活卡片 x${ - //_/\__/_/ \_,_/\__/\__/\_,_/ - cardLastFour - }。一切准备就绪,可以继续报销了!`, + `已清除可疑活动并重新激活卡片 x${cardLastFour}。一切准备就绪,可以继续报销了!`, deactivatedMessage: ({cardLastFour}: {cardLastFour: string}) => `已停用以${cardLastFour}结尾的卡片`, alertMessage: ({ cardLastFour, @@ -2002,7 +1999,7 @@ const translations = { date: string; }) => `在卡号以${cardLastFour}结尾的卡上发现可疑活动。您是否认可此笔费用? -${date},${merchant},金额为${amount}。`, +${merchant}的${amount} - ${date}`, }, }, workflowsPage: { From be9aa75ba5da333c771b3468ba7160e7aae6e1a0 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Wed, 8 Oct 2025 15:22:42 -1000 Subject: [PATCH 12/14] prettier --- src/languages/zh-hans.ts | 3 +-- src/libs/ReportActionsUtils.ts | 5 ++++- src/libs/ReportUtils.ts | 4 +--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index dfd20d39bbf42..67fda00de6850 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -1984,8 +1984,7 @@ const translations = { cardFraudAlert: { confirmButtonText: '是的,我愿意。', reportFraudButtonText: '不,不是我', - clearedMessage: ({cardLastFour}: {cardLastFour: string}) => - `已清除可疑活动并重新激活卡片 x${cardLastFour}。一切准备就绪,可以继续报销了!`, + clearedMessage: ({cardLastFour}: {cardLastFour: string}) => `已清除可疑活动并重新激活卡片 x${cardLastFour}。一切准备就绪,可以继续报销了!`, deactivatedMessage: ({cardLastFour}: {cardLastFour: string}) => `已停用以${cardLastFour}结尾的卡片`, alertMessage: ({ cardLastFour, diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 3de8e0da4a326..ad214ac52d25f 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -883,7 +883,10 @@ function shouldReportActionBeVisible(reportAction: OnyxEntry, key: } if ( - (isActionableReportMentionWhisper(reportAction) || isActionableJoinRequestPendingReportAction(reportAction) || isActionableMentionWhisper(reportAction) || isActionableCardFraudAlert(reportAction)) && + (isActionableReportMentionWhisper(reportAction) || + isActionableJoinRequestPendingReportAction(reportAction) || + isActionableMentionWhisper(reportAction) || + isActionableCardFraudAlert(reportAction)) && !canUserPerformWriteAction ) { return false; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 119b28b4bc71a..d435fd61a93b6 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -3825,9 +3825,7 @@ type ReasonAndReportActionThatRequiresAttention = { function getUnresolvedCardFraudAlertAction(reportID: string): OnyxEntry { const reportActions = getAllReportActions(reportID); - return Object.values(reportActions).find( - (action): action is ReportAction => isActionableCardFraudAlert(action) && !getOriginalMessage(action)?.resolution - ); + return Object.values(reportActions).find((action): action is ReportAction => isActionableCardFraudAlert(action) && !getOriginalMessage(action)?.resolution); } function hasUnresolvedCardFraudAlert(reportOrOption: OnyxEntry | OptionData): boolean { From db80f8e201545ad73ed58013e8f5999acbe0e221 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Wed, 8 Oct 2025 15:30:48 -1000 Subject: [PATCH 13/14] fix types --- src/languages/en.ts | 1 + src/languages/es.ts | 11 +++++++---- src/libs/API/types.ts | 1 + 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 1eab7c997ad16..52bece601d0fa 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7107,6 +7107,7 @@ const translations = { isWaitingForAssigneeToCompleteAction: 'Is waiting for assignee to complete action', hasChildReportAwaitingAction: 'Has child report awaiting action', hasMissingInvoiceBankAccount: 'Has missing invoice bank account', + hasUnresolvedCardFraudAlert: 'Has unresolved card fraud alert', }, reasonRBR: { hasErrors: 'Has errors in report or report actions data', diff --git a/src/languages/es.ts b/src/languages/es.ts index 0ffe70cc65044..c696c1597b163 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1992,8 +1992,10 @@ const translations = { enterMagicCode: ({contactMethod}: EnterMagicCodeParams) => `Introduzca el código mágico enviado a ${contactMethod} para ver los datos de su tarjeta. Debería llegar en un par de minutos.`, cardFraudAlert: { - clearedMessage: "[es] cleared the earlier suspicious activity. The card is reactivated. You're all set to keep on expensin'!", - deactivatedMessage: '[es] the card has been deactivated.', + confirmButtonText: 'Sí, lo hago', + reportFraudButtonText: 'No, no fui yo', + clearedMessage: ({cardLastFour}: {cardLastFour: string}) => `se eliminó la actividad sospechosa anterior y se reactivó la tarjeta x${cardLastFour}. ¡Todo listo para seguir gastando!`, + deactivatedMessage: ({cardLastFour}: {cardLastFour: string}) => `la tarjeta terminada en ${cardLastFour} ha sido desactivada`, alertMessage: ({ cardLastFour, amount, @@ -2004,9 +2006,9 @@ const translations = { amount: string; merchant: string; date: string; - }) => `[es] identified suspicious activity on card ending in ${cardLastFour}. Do you recognize this charge? + }) => `se identificó actividad sospechosa en la tarjeta terminada en ${cardLastFour}. ¿Reconoces este cargo? -${amount} for ${merchant} - ${date}`, +${amount} para ${merchant} - ${date}`, }, }, workflowsPage: { @@ -7596,6 +7598,7 @@ ${amount} for ${merchant} - ${date}`, isWaitingForAssigneeToCompleteAction: 'Esperando a que el asignado complete la acción', hasChildReportAwaitingAction: 'Informe secundario pendiente de acción', hasMissingInvoiceBankAccount: 'Falta la cuenta bancaria de la factura', + hasUnresolvedCardFraudAlert: 'Tiene alerta de fraude de tarjeta sin resolver', }, reasonRBR: { hasErrors: 'Tiene errores en los datos o las acciones del informe', diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 2809c0c7e0db9..a121778cb5dc9 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -535,6 +535,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.BANK_ACCOUNT_HANDLE_PLAID_ERROR]: Parameters.BankAccountHandlePlaidErrorParams; [WRITE_COMMANDS.REPORT_VIRTUAL_EXPENSIFY_CARD_FRAUD]: Parameters.ReportVirtualExpensifyCardFraudParams; [WRITE_COMMANDS.REQUEST_REPLACEMENT_EXPENSIFY_CARD]: Parameters.RequestReplacementExpensifyCardParams; + [WRITE_COMMANDS.RESOLVE_FRAUD_ALERT]: Parameters.ResolveFraudAlertParams; [WRITE_COMMANDS.UPDATE_EXPENSIFY_CARD_LIMIT]: Parameters.UpdateExpensifyCardLimitParams; [WRITE_COMMANDS.UPDATE_EXPENSIFY_CARD_TITLE]: Parameters.UpdateExpensifyCardTitleParams; [WRITE_COMMANDS.UPDATE_EXPENSIFY_CARD_LIMIT_TYPE]: Parameters.UpdateExpensifyCardLimitTypeParams; From 51d4668cdbdcfd1923347421c471b64beeb091b1 Mon Sep 17 00:00:00 2001 From: Marc Glasser Date: Wed, 8 Oct 2025 15:31:16 -1000 Subject: [PATCH 14/14] Export ResolveFraudAlertParams --- src/libs/API/parameters/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index f0299c943bc8f..983b3d7d0e4f3 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -63,6 +63,7 @@ export type {default as PusherPingParams} from './PusherPingParams'; export type {default as ReconnectAppParams} from './ReconnectAppParams'; export type {default as ReferTeachersUniteVolunteerParams} from './ReferTeachersUniteVolunteerParams'; export type {default as ReportVirtualExpensifyCardFraudParams} from './ReportVirtualExpensifyCardFraudParams'; +export type {default as ResolveFraudAlertParams} from './ResolveFraudAlertParams'; export type {default as RequestContactMethodValidateCodeParams} from './RequestContactMethodValidateCodeParams'; export type {default as RequestNewValidateCodeParams} from './RequestNewValidateCodeParams'; export type {default as RequestReplacementExpensifyCardParams} from './RequestReplacementExpensifyCardParams';