Skip to content

Conversation

@mountiny
Copy link
Contributor

@mountiny mountiny commented Jan 30, 2026

Explanation of Change

Details

This PR implements the Card Fraud Alert time-sensitive widget for the Home page as part of Release 3.

When a user's Expensify Card has potential fraud detected (either individual-level or domain-level), a high-priority alert widget appears in the Time Sensitive section, prompting them to review the suspicious activity.

Changes

New Component: ReviewCardFraud.tsx

  • Displays a time-sensitive widget when card.fraud === 'domain' or card.fraud === 'individual'
  • Fetches fraud details (amount, merchant, currency) from the associated report action
  • Uses CurrencyUtils.convertToDisplayString() to format the transaction amount
  • Shows dynamic title: "Review $500 in potential fraud at Gucci" when details are available
  • Falls back to generic title: "Review potential fraud on your Expensify Card" if details not loaded
  • Navigates to the fraud alert report on CTA press
  • Uses danger styling (tangerine background, red button) to indicate urgency

Type Extensions: Card.ts

  • Added PossibleFraudData type with fraudAlertReportID and fraudAlertReportActionID for deeplink navigation
  • Added CardMessage type containing possibleFraud data
  • Extended Card type with optional message field

Selector Updates: Card.ts

  • Added isCardWithPotentialFraud() helper function
  • Extended timeSensitiveCardsSelector to return cardsWithFraud array

Hook Updates: useTimeSensitiveCards.ts

  • Added cardsWithFraud and shouldShowReviewCardFraud to return values

TimeSensitiveSection Updates

  • Renders fraud alert widget with highest priority (before discount offers, shipping, activation)

Fixed Issues

$ #79992
PROPOSAL:

Tests

Prerequisites:
Logged in user with an Expensify Card

Test Steps

  1. Apply mock data - Open browser DevTools console and run:
Onyx.merge('cardList', { '12345': { cardID: 12345, accountID: 18439984, bank: 'Expensify Card', availableSpend: 5000, domainName: 'expensify-policy123.exfy', fraud: 'individual', fundID: '123456', lastFourPAN: '1234', state: 3, isVirtual: false, nameValuePairs: { isVirtual: false }, message: { possibleFraud: { state: 1, date: '2025-01-29', fraudAlertReportID: 999888777, fraudAlertReportActionID: 111222333 } } } });

Onyx.merge('reportActions_999888777', { '111222333': { reportActionID: '111222333', actionName: 'ACTIONABLECARDFAUDALERT', created: '2025-01-29 10:00:00.000', actorAccountID: 1, originalMessage: { cardID: 12345, maskedCardNumber: 'XXXXXXXXXXXX1234', triggerAmount: 50000, triggerMerchant: 'Gucci', currency: 'USD' } } });
  1. Navigate to Home page
  2. Verify widget appears in the Time Sensitive section with:
  • Expensify Card icon with tangerine background
  • Title: "Review $500.00 in potential fraud at Gucci"
  • Subtitle: "Expensify Card"
  • Red "Review" button
  1. Click "Review" button - should navigate to the fraud alert report
  2. Test fallback title - Apply mock without report action:
Onyx.merge('cardList', { '99999': { cardID: 99999, accountID: 18439984, bank: 'Expensify Card', fraud: 'domain', fundID: '123456', state: 3, nameValuePairs: { isVirtual: false } } });
  1. Should show generic title: "Review potential fraud on your Expensify Card"
  2. Test priority - Verify fraud widget appears above discount offers and card activation widgets
  3. Test exclusion - Cards with fraud: 'none' should NOT show the widget
  • Verify that no errors appear in the JS console

Offline tests

N/A

QA Steps

Same as steps
// TODO: These must be filled out, or the issue title must include "[No QA]."

  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I verified there are no new alerts related to the canBeMissing param for useOnyx
  • I followed proper code patterns (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text shown in the product is localized by adding it to src/languages/* files and using the translation method
      • If any non-english text was added/modified, I used JaimeGPT to get English > Spanish translation. I then posted it in #expensify-open-source and it was approved by an internal Expensify engineer. Link to Slack message:
    • I verified all numbers, amounts, dates and phone numbers shown in the product are using the localization methods
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
    • I verified proper file naming conventions were followed for any new files or renamed files. All non-platform specific files are named after what they export and are not named "index.js". All platform-specific files are named for the platform the code supports as outlined in the README.
    • I verified the JSDocs style guidelines (in STYLE.md) were followed
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests)
  • I verified any variables that can be defined as constants (ie. in CONST.ts or at the top of the file that uses the constant) are defined as such
  • I verified that if a function's arguments changed that all usages have also been updated correctly
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • If a new page is added, I verified it's using the ScrollView component to make it scrollable when more elements are added to the page.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.
  • I verified that similar component doesn't exist in the codebase
  • I verified that all props are defined accurately and each prop has a /** comment above it */
  • I verified that each file is named correctly
  • I verified that each component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
  • I verified that the only data being stored in component state is data necessary for rendering and nothing else
  • In component if we are not using the full Onyx data that we loaded, I've added the proper selector in order to ensure the component only re-renders when the data it is using changes
  • For Class Components, any internal methods passed to components event handlers are bound to this properly so there are no scoping issues (i.e. for onClick={this.submit} the method this.submit should be bound to this in the constructor)
  • I verified that component internal methods bound to this are necessary to be bound (i.e. avoid this.submit = this.submit.bind(this); if this.submit is never passed to a component event handler like onClick)
  • I verified that all JSX used for rendering exists in the render method
  • I verified that each component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions

Screenshots/Videos

Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari

@OSBotify

This comment has been minimized.

@mountiny mountiny added the InternalQA This pull request required internal QA label Jan 30, 2026
@OSBotify
Copy link
Contributor

🦜 Polyglot Parrot! 🦜

Squawk! Looks like you added some shiny new English strings. Allow me to parrot them back to you in other tongues:

View the translation diff
diff --git a/src/languages/de.ts b/src/languages/de.ts
index eb14d022..22442d42 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -7240,7 +7240,7 @@ Fordere Spesendetails wie Belege und Beschreibungen an, lege Limits und Standard
                 addedConnection: ({connectionName}: ConnectionNameParams) => `verbunden mit ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]}`,
                 leftTheChat: 'hat den Chat verlassen',
                 companyCardConnectionBroken: ({feedName, workspaceCompanyCardRoute}: {feedName: string; workspaceCompanyCardRoute: string}) =>
-                    `Die Verbindung zu ${feedName} ist unterbrochen. Um Kartenimporte wiederherzustellen, <a href='${workspaceCompanyCardRoute}'>melden Sie sich bei Ihrer Bank an</a>`,
+                    `Die ${feedName}-Verbindung ist unterbrochen. Um Kartenimporte wiederherzustellen, <a href='${workspaceCompanyCardRoute}'>melden Sie sich bei Ihrer Bank an</a>`,
                 plaidBalanceFailure: ({maskedAccountNumber, walletRoute}: {maskedAccountNumber: string; walletRoute: string}) =>
                     `die Plaid-Verbindung zu Ihrem Geschäftsbankkonto ist unterbrochen. Bitte <a href='${walletRoute}'>verbinden Sie Ihr Bankkonto ${maskedAccountNumber} erneut</a>, damit Sie Ihre Expensify-Karten weiterhin nutzen können.`,
                 settlementAccountLocked: ({maskedBankAccountNumber}: OriginalMessageSettlementAccountLocked, linkURL: string) =>
@@ -8308,8 +8308,8 @@ Hier ist ein *Testbeleg*, um dir zu zeigen, wie es funktioniert:`,
             addShippingAddress: {title: 'Wir benötigen Ihre Versandadresse', subtitle: 'Gib eine Adresse an, um deine Expensify Card zu erhalten.', cta: 'Adresse hinzufügen'},
             activateCard: {title: 'Aktiviere deine Expensify Card', subtitle: 'Bestätige deine Karte und beginne mit dem Ausgeben.', cta: 'Aktivieren'},
             reviewCardFraud: {
-                title: 'Verdächtige Aktivitäten auf Ihrer Expensify Card überprüfen',
-                titleWithDetails: ({amount, merchant}: {amount: string; merchant: string}) => `Überprüfe ${amount} an potenziellem Betrug bei ${merchant}`,
+                title: 'Möglichen Betrug mit Ihrer Expensify Card überprüfen',
+                titleWithDetails: ({amount, merchant}: {amount: string; merchant: string}) => `Überprüfen Sie potenziellen Betrug in Höhe von ${amount} bei ${merchant}`,
                 subtitle: 'Expensify Card',
                 cta: 'Überprüfen',
             },
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index e2a204e1..9626c0bc 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -7252,7 +7252,7 @@ Exigez des informations de dépense comme les reçus et les descriptions, défin
                 addedConnection: ({connectionName}: ConnectionNameParams) => `connecté à ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]}`,
                 leftTheChat: 'a quitté la discussion',
                 companyCardConnectionBroken: ({feedName, workspaceCompanyCardRoute}: {feedName: string; workspaceCompanyCardRoute: string}) =>
-                    `La connexion ${feedName} est interrompue. Pour rétablir l’importation des cartes, <a href='${workspaceCompanyCardRoute}'>connectez-vous à votre banque</a>`,
+                    `La connexion ${feedName} est rompue. Pour rétablir l’importation des cartes, <a href='${workspaceCompanyCardRoute}'>connectez-vous à votre banque</a>`,
                 plaidBalanceFailure: ({maskedAccountNumber, walletRoute}: {maskedAccountNumber: string; walletRoute: string}) =>
                     `la connexion Plaid à votre compte bancaire professionnel est interrompue. Veuillez <a href='${walletRoute}'>reconnecter votre compte bancaire ${maskedAccountNumber}</a> pour continuer à utiliser vos cartes Expensify.`,
                 settlementAccountLocked: ({maskedBankAccountNumber}: OriginalMessageSettlementAccountLocked, linkURL: string) =>
@@ -8314,7 +8314,7 @@ Voici un *reçu test* pour vous montrer comment cela fonctionne :`,
             addShippingAddress: {title: 'Nous avons besoin de votre adresse de livraison', subtitle: 'Indiquez une adresse pour recevoir votre carte Expensify.', cta: 'Ajouter une adresse'},
             activateCard: {title: 'Activer votre carte Expensify', subtitle: 'Validez votre carte et commencez à dépenser.', cta: 'Activer'},
             reviewCardFraud: {
-                title: 'Examiner un éventuel cas de fraude sur votre carte Expensify',
+                title: 'Examiner une fraude potentielle sur votre carte Expensify',
                 titleWithDetails: ({amount, merchant}: {amount: string; merchant: string}) => `Examiner ${amount} de fraude potentielle chez ${merchant}`,
                 subtitle: 'Carte Expensify',
                 cta: 'Vérifier',
diff --git a/src/languages/it.ts b/src/languages/it.ts
index 87561d9d..95a5fbdf 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -772,7 +772,7 @@ const translations: TranslationDeepObject<typeof en> = {
             revoke: 'Revoca',
             title: 'Riconoscimento facciale/impronta digitale & passkey',
             explanation:
-                'La verifica con volto/impronta o passkey è abilitata su uno o più dispositivi. La revoca dell’accesso richiederà un codice magico per la prossima verifica su qualsiasi dispositivo',
+                'La verifica tramite volto/impronta digitale o passkey è abilitata su uno o più dispositivi. Revocare l’accesso richiederà un codice magico per la prossima verifica su qualsiasi dispositivo',
             confirmationPrompt: 'Sei sicuro? Avrai bisogno di un codice magico per la prossima verifica su qualsiasi dispositivo',
             cta: 'Revoca accesso',
             noDevices: 'Non hai alcun dispositivo registrato per la verifica con volto/impronta digitale o passkey. Se ne registri uno, potrai revocare tale accesso da qui.',
@@ -8294,8 +8294,8 @@ Ecco una *ricevuta di prova* per mostrarti come funziona:`,
             addShippingAddress: {title: 'Abbiamo bisogno del tuo indirizzo di spedizione', subtitle: 'Fornisci un indirizzo per ricevere la tua Expensify Card.', cta: 'Aggiungi indirizzo'},
             activateCard: {title: 'Attiva la tua Expensify Card', subtitle: 'Convalida la tua carta e inizia a spendere.', cta: 'Attiva'},
             reviewCardFraud: {
-                title: 'Esamina potenziali frodi sulla tua Expensify Card',
-                titleWithDetails: ({amount, merchant}: {amount: string; merchant: string}) => `Esamina ${amount} di potenziale frode presso ${merchant}`,
+                title: 'Esamina una potenziale frode sulla tua Expensify Card',
+                titleWithDetails: ({amount, merchant}: {amount: string; merchant: string}) => `Controlla ${amount} di potenziale frode presso ${merchant}`,
                 subtitle: 'Carta Expensify',
                 cta: 'Rivedi',
             },
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index ad8ae42d..5022c6de 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -770,9 +770,8 @@ const translations: TranslationDeepObject<typeof en> = {
         revoke: {
             revoke: '取り消す',
             title: '顔/指紋 & パスキー',
-            explanation:
-                '1 つ以上のデバイスで、顔 / 指紋またはパスキーによる認証が有効になっています。アクセスを取り消すと、次回どのデバイスであっても、認証にはマジックコードが必要になります',
-            confirmationPrompt: '本当に実行しますか?今後、どのデバイスでも次回の認証にはマジックコードが必要になります',
+            explanation: '1 台以上のデバイスで顔/指紋またはパスキーによる認証が有効になっています。アクセスを取り消すと、次回どのデバイスで認証する場合でもマジックコードが必要になります',
+            confirmationPrompt: '本当に続行しますか?今後どのデバイスでも次回の認証にはマジックコードが必要になります',
             cta: 'アクセスを取り消す',
             noDevices: '顔認証 / 指紋認証 またはパスキー認証用に登録されているデバイスがありません。  \nいずれかを登録すると、ここでそのアクセスを取り消せるようになります。',
             dismiss: '了解',
@@ -7167,7 +7166,7 @@ ${reportName}
                 addedConnection: ({connectionName}: ConnectionNameParams) => `${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]} に接続済み`,
                 leftTheChat: 'チャットを退出しました',
                 companyCardConnectionBroken: ({feedName, workspaceCompanyCardRoute}: {feedName: string; workspaceCompanyCardRoute: string}) =>
-                    `${feedName} との接続が切断されています。カードの取引明細の取り込みを再開するには、<a href='${workspaceCompanyCardRoute}'>銀行にログイン</a>してください`,
+                    `${feedName} の接続が切断されています。カードの取込を再開するには、<a href='${workspaceCompanyCardRoute}'>銀行にログイン</a>してください`,
                 plaidBalanceFailure: ({maskedAccountNumber, walletRoute}: {maskedAccountNumber: string; walletRoute: string}) =>
                     `ビジネス銀行口座へのPlaid接続が切断されています。Expensifyカードを引き続きご利用いただくには、<a href='${walletRoute}'>銀行口座 ${maskedAccountNumber} を再接続</a>してください。`,
                 settlementAccountLocked: ({maskedBankAccountNumber}: OriginalMessageSettlementAccountLocked, linkURL: string) =>
@@ -8207,8 +8206,8 @@ Expensify の使い方をお見せするための*テストレシート*がこ
             activateCard: {title: 'Expensify Card を有効化', subtitle: 'カードを認証して、すぐに支出を始めましょう。', cta: '有効化'},
             reviewCardFraud: {
                 title: 'Expensify Card の不正利用の可能性を確認する',
-                titleWithDetails: ({amount, merchant}: {amount: string; merchant: string}) => `${merchant} における潜在的な不正行為の ${amount} を確認`,
-                subtitle: 'Expensify カード',
+                titleWithDetails: ({amount, merchant}: {amount: string; merchant: string}) => `${merchant} での不正の可能性がある ${amount} を確認`,
+                subtitle: 'Expensify Card',
                 cta: 'レビュー',
             },
         },
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index 6636a8ad..56534a11 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -772,7 +772,7 @@ const translations: TranslationDeepObject<typeof en> = {
             revoke: 'Intrekken',
             title: 'Gezicht/vingerafdruk en passkeys',
             explanation:
-                'Gezichts-/vingerafdruk- of passkeys-verificatie is ingeschakeld op één of meer apparaten. Toegang intrekken vereist een magische code voor de volgende verificatie op elk apparaat',
+                'Gezichts-/vingerafdruk- of passkeys-verificatie is ingeschakeld op één of meer apparaten. Toegang intrekken betekent dat bij de volgende verificatie op elk apparaat een magische code vereist is',
             confirmationPrompt: 'Weet je het zeker? Je hebt een magische code nodig voor de volgende verificatie op elk apparaat',
             cta: 'Toegang intrekken',
             noDevices: 'Je hebt geen apparaten geregistreerd voor gezichts-/vingerafdruk- of passkey-verificatie. Als je er een registreert, kun je die toegang hier intrekken.',
@@ -7212,7 +7212,7 @@ Vraag verplichte uitgavedetails zoals bonnetjes en beschrijvingen, stel limieten
                 addedConnection: ({connectionName}: ConnectionNameParams) => `verbonden met ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]}`,
                 leftTheChat: 'heeft de chat verlaten',
                 companyCardConnectionBroken: ({feedName, workspaceCompanyCardRoute}: {feedName: string; workspaceCompanyCardRoute: string}) =>
-                    `De ${feedName}-verbinding is verbroken. Om kaartimports te herstellen, <a href='${workspaceCompanyCardRoute}'>log in bij uw bank</a>`,
+                    `De ${feedName}-verbinding is verbroken. Om kaartimports te herstellen, <a href='${workspaceCompanyCardRoute}'>log in bij je bank</a>`,
                 plaidBalanceFailure: ({maskedAccountNumber, walletRoute}: {maskedAccountNumber: string; walletRoute: string}) =>
                     `de Plaid-verbinding met uw zakelijke bankrekening is verbroken. <a href='${walletRoute}'>Verbind uw bankrekening ${maskedAccountNumber} opnieuw</a> om uw Expensify-kaarten te kunnen blijven gebruiken.`,
                 settlementAccountLocked: ({maskedBankAccountNumber}: OriginalMessageSettlementAccountLocked, linkURL: string) =>
@@ -8273,7 +8273,7 @@ Hier is een *testbon* om je te laten zien hoe het werkt:`,
                 title: 'Controleer mogelijke fraude op je Expensify Card',
                 titleWithDetails: ({amount, merchant}: {amount: string; merchant: string}) => `Controleer ${amount} aan mogelijke fraude bij ${merchant}`,
                 subtitle: 'Expensify Card',
-                cta: 'Beoordelen',
+                cta: 'Beoordeling',
             },
         },
     },
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index 273180ab..bead3e6c 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -772,8 +772,8 @@ const translations: TranslationDeepObject<typeof en> = {
             revoke: 'Odwołaj',
             title: 'Rozpoznawanie twarzy/odcisk palca i klucze dostępu',
             explanation:
-                'Weryfikacja za pomocą twarzy/odcisku palca lub klucza dostępu jest włączona na jednym lub większej liczbie urządzeń. Cofnięcie dostępu spowoduje, że przy następnej weryfikacji na dowolnym urządzeniu wymagany będzie magiczny kod',
-            confirmationPrompt: 'Czy na pewno? Będziesz potrzebować magicznego kodu do następnej weryfikacji na dowolnym urządzeniu',
+                'Weryfikacja twarzą/odciskiem palca lub kluczem dostępowa jest włączona na jednym lub większej liczbie urządzeń. Odwołanie dostępu spowoduje, że przy następnej weryfikacji na dowolnym urządzeniu wymagany będzie magiczny kod',
+            confirmationPrompt: 'Czy na pewno? Będziesz potrzebować kodu magicznego do kolejnej weryfikacji na dowolnym urządzeniu',
             cta: 'Cofnij dostęp',
             noDevices:
                 'Nie masz żadnych urządzeń zarejestrowanych do weryfikacji twarzą, odciskiem palca ani kluczem dostępu. Jeśli jakieś zarejestrujesz, będziesz mógł/mogła cofnąć ten dostęp w tym miejscu.',
@@ -8255,7 +8255,7 @@ Oto *paragon testowy*, który pokazuje, jak to działa:`,
             activateCard: {title: 'Aktywuj swoją kartę Expensify', subtitle: 'Zweryfikuj swoją kartę i zacznij wydawać.', cta: 'Aktywuj'},
             reviewCardFraud: {
                 title: 'Przejrzyj potencjalne oszustwo na swojej karcie Expensify',
-                titleWithDetails: ({amount, merchant}: {amount: string; merchant: string}) => `Sprawdź ${amount} pod kątem potencjalnego oszustwa u ${merchant}`,
+                titleWithDetails: ({amount, merchant}: {amount: string; merchant: string}) => `Sprawdź ${amount} pod kątem potencjalnego oszustwa w ${merchant}`,
                 subtitle: 'Karta Expensify',
                 cta: 'Przejrzyj',
             },
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index 430b1dbb..3c8cb34c 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -771,7 +771,7 @@ const translations: TranslationDeepObject<typeof en> = {
             revoke: 'Revogar',
             title: 'Rosto/digital & chaves de acesso',
             explanation:
-                'A verificação por rosto/digital ou por chave de acesso está ativada em um ou mais dispositivos. Revogar o acesso exigirá um código mágico para a próxima verificação em qualquer dispositivo',
+                'A verificação por rosto/digital ou chave de acesso está ativada em um ou mais dispositivos. Revogar o acesso exigirá um código mágico para a próxima verificação em qualquer dispositivo',
             confirmationPrompt: 'Tem certeza? Você precisará de um código mágico para a próxima verificação em qualquer dispositivo',
             cta: 'Revogar acesso',
             noDevices: 'Você não tem nenhum dispositivo registrado para verificação por rosto/digital ou passkey. Se você registrar algum, poderá revogar esse acesso aqui.',
@@ -7201,7 +7201,7 @@ Exija detalhes de despesas como recibos e descrições, defina limites e padrõe
                 addedConnection: ({connectionName}: ConnectionNameParams) => `conectado a ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]}`,
                 leftTheChat: 'saiu do chat',
                 companyCardConnectionBroken: ({feedName, workspaceCompanyCardRoute}: {feedName: string; workspaceCompanyCardRoute: string}) =>
-                    `A conexão ${feedName} está quebrada. Para restaurar as importações de cartão, <a href='${workspaceCompanyCardRoute}'>faça login no seu banco</a>`,
+                    `A conexão com ${feedName} está quebrada. Para restaurar as importações de cartão, <a href='${workspaceCompanyCardRoute}'>faça login no seu banco</a>`,
                 plaidBalanceFailure: ({maskedAccountNumber, walletRoute}: {maskedAccountNumber: string; walletRoute: string}) =>
                     `a conexão Plaid com sua conta bancária empresarial está quebrada. Por favor, <a href='${walletRoute}'>reconecte sua conta bancária ${maskedAccountNumber}</a> para continuar usando seus Cartões Expensify.`,
                 settlementAccountLocked: ({maskedBankAccountNumber}: OriginalMessageSettlementAccountLocked, linkURL: string) =>
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index c99431e0..1b68279a 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -766,8 +766,8 @@ const translations: TranslationDeepObject<typeof en> = {
         revoke: {
             revoke: '撤销',
             title: '面部识别/指纹识别与通行密钥',
-            explanation: '在一台或多台设备上已启用面部/指纹或通行密钥验证。撤销访问后,下次在任意设备上进行验证时都需要使用魔法代码',
-            confirmationPrompt: '您确定吗?您在任何设备上的下一次验证都需要一个魔法代码',
+            explanation: '在一台或多台设备上已启用面容/指纹或通行密钥验证。撤销访问权限后,下次在任意设备上进行验证时都需要使用魔法验证码',
+            confirmationPrompt: '你确定吗?接下来在任何设备上进行验证时,你都需要一个魔法验证码',
             cta: '撤销访问权限',
             noDevices: '您尚未注册任何用于人脸/指纹或通行密钥验证的设备。如果您注册了设备,您将可以在此撤销其访问权限。',
             dismiss: '明白了',
@@ -7040,7 +7040,7 @@ ${reportName}
                 addedConnection: ({connectionName}: ConnectionNameParams) => `已连接到 ${CONST.POLICY.CONNECTIONS.NAME_USER_FRIENDLY[connectionName]}`,
                 leftTheChat: '已离开聊天',
                 companyCardConnectionBroken: ({feedName, workspaceCompanyCardRoute}: {feedName: string; workspaceCompanyCardRoute: string}) =>
-                    `${feedName} 连接已中断。若要恢复卡片导入,请<a href='${workspaceCompanyCardRoute}'>登录您的银行账户</a>`,
+                    `${feedName} 连接已中断。若要恢复导入卡片交易,请<a href='${workspaceCompanyCardRoute}'>登录您的银行账户</a>`,
                 plaidBalanceFailure: ({maskedAccountNumber, walletRoute}: {maskedAccountNumber: string; walletRoute: string}) =>
                     `您的企业银行账户的 Plaid 连接已中断。请<a href='${walletRoute}'>重新连接您的银行账户 ${maskedAccountNumber}</a>,以便继续使用您的 Expensify 卡。`,
                 settlementAccountLocked: ({maskedBankAccountNumber}: OriginalMessageSettlementAccountLocked, linkURL: string) =>
@@ -8022,8 +8022,8 @@ ${reportName}
             addShippingAddress: {title: '我们需要您的收货地址', subtitle: '请提供一个地址以接收您的 Expensify Card。', cta: '添加地址'},
             activateCard: {title: '激活您的 Expensify Card', subtitle: '验证您的卡片并开始消费。', cta: '激活'},
             reviewCardFraud: {
-                title: '查看您的 Expensify Card 上的潜在欺诈行为',
-                titleWithDetails: ({amount, merchant}: {amount: string; merchant: string}) => `在 ${merchant} 审核可能存在欺诈的金额 ${amount}`,
+                title: '审查您的 Expensify Card 潜在欺诈行为',
+                titleWithDetails: ({amount, merchant}: {amount: string; merchant: string}) => `在 ${merchant} 发现疑似欺诈金额 ${amount},请审核`,
                 subtitle: 'Expensify Card',
                 cta: '审核',
             },

Note

You can apply these changes to your branch by copying the patch to your clipboard, then running pbpaste | git apply 😉

View workflow run

}

// Only consider Expensify cards
const isExpensifyCard = card.bank === CONST.EXPENSIFY_CARD.BANK;
Copy link
Contributor

@ZhenjaHorbach ZhenjaHorbach Jan 30, 2026

Choose a reason for hiding this comment

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

Let's use isExpensifyCard utility!

@mountiny mountiny marked this pull request as ready for review January 30, 2026 23:24
@mountiny mountiny requested review from a team as code owners January 30, 2026 23:24
@melvin-bot melvin-bot bot requested review from ZhenjaHorbach and joekaufmanexpensify and removed request for a team and joekaufmanexpensify January 30, 2026 23:24
@melvin-bot
Copy link

melvin-bot bot commented Jan 30, 2026

@ZhenjaHorbach Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@melvin-bot melvin-bot bot removed the request for review from a team January 30, 2026 23:24
const fraudAlertReportActionID = card.message?.possibleFraud?.fraudAlertReportActionID;

// Fetch the report actions to get the fraud details (amount, merchant, currency)
const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${fraudAlertReportID}`, {
Copy link
Contributor

Choose a reason for hiding this comment

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

PERF-11 (docs)

The useOnyx hook fetches the entire REPORT_ACTIONS collection for the fraud alert report, which can cause unnecessary re-renders when any action in that report changes. Since only a specific action is needed (identified by fraudAlertReportActionID), this causes the component to re-render for unrelated report action changes.

Suggested fix: Use a selector to extract only the specific fraud alert action:

const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${fraudAlertReportID}`, {
    canBeMissing: true,
    selector: (actions) => (fraudAlertReportActionID && actions ? {[fraudAlertReportActionID]: actions[fraudAlertReportActionID]} : null),
});

This ensures the component only re-renders when the specific fraud alert action changes, not when any action in the report changes.


Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

}

// Only consider Expensify cards
const isExpensifyCard = card.bank === CONST.EXPENSIFY_CARD.BANK;
Copy link
Contributor

Choose a reason for hiding this comment

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

PERF-13 (docs)

The constant CONST.EXPENSIFY_CARD.BANK is accessed inside the loop on every iteration, but it is iterator-independent and produces the same value regardless of which card is being processed. This creates O(n) overhead.

Suggested fix: Hoist the constant access outside the loop:

const timeSensitiveCardsSelector = (cards: OnyxEntry<CardList>): TimeSensitiveCardsResult => {
    const result: TimeSensitiveCardsResult = {
        cardsNeedingShippingAddress: [],
        cardsNeedingActivation: [],
        cardsWithFraud: [],
    };

    const expensifyCardBank = CONST.EXPENSIFY_CARD.BANK;

    for (const card of Object.values(cards ?? {})) {
        if (!isCard(card)) {
            continue;
        }

        // Only consider Expensify cards
        const isExpensifyCard = card.bank === expensifyCardBank;
        // ... rest of the logic
    }

    return result;
};

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

* Check if a card has potential fraud that needs review.
* Returns true if the card has fraud type 'domain' or 'individual'.
*/
const isCardWithPotentialFraud = (card: Card): boolean => {
Copy link
Contributor

Choose a reason for hiding this comment

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

PERF-13 (docs)

The constants CONST.EXPENSIFY_CARD.FRAUD_TYPES.DOMAIN and CONST.EXPENSIFY_CARD.FRAUD_TYPES.INDIVIDUAL are accessed within the isCardWithPotentialFraud function, which is called inside a loop (via timeSensitiveCardsSelector). While the function itself is lightweight, these constant accesses happen on every card iteration.

Suggested fix: Hoist the fraud type constants outside the function:

const FRAUD_TYPE_DOMAIN = CONST.EXPENSIFY_CARD.FRAUD_TYPES.DOMAIN;
const FRAUD_TYPE_INDIVIDUAL = CONST.EXPENSIFY_CARD.FRAUD_TYPES.INDIVIDUAL;

/**
 * Check if a card has potential fraud that needs review.
 * Returns true if the card has fraud type 'domain' or 'individual'.
 */
const isCardWithPotentialFraud = (card: Card): boolean => {
    return card.fraud === FRAUD_TYPE_DOMAIN || card.fraud === FRAUD_TYPE_INDIVIDUAL;
};

This avoids repeated constant property lookups during iteration.


Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

}

const fraudAction = reportActions[fraudAlertReportActionID];
if (!fraudAction || fraudAction.actionName !== CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_CARD_FRAUD_ALERT) {
Copy link
Contributor

Choose a reason for hiding this comment

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

PERF-13 (docs)

The constant CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_CARD_FRAUD_ALERT is accessed inside the useMemo callback, which recalculates whenever fraudAlertReportActionID or reportActions changes. This constant access is iterator-independent (doesn't depend on the computation) and should be hoisted outside.

Suggested fix: Hoist the constant outside the useMemo:

function ReviewCardFraud({card}: ReviewCardFraudProps) {
    const theme = useTheme();
    const {translate} = useLocalize();

    // Get the fraud alert report ID and action ID for deeplink navigation
    const fraudAlertReportID = card.message?.possibleFraud?.fraudAlertReportID;
    const fraudAlertReportActionID = card.message?.possibleFraud?.fraudAlertReportActionID;

    // Fetch the report actions to get the fraud details (amount, merchant, currency)
    const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${fraudAlertReportID}`, {
        canBeMissing: true,
    });

    // Extract fraud details from the report action
    const fraudActionType = CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_CARD_FRAUD_ALERT;
    const fraudDetails = useMemo(() => {
        if (\!fraudAlertReportActionID || \!reportActions) {
            return null;
        }

        const fraudAction = reportActions[fraudAlertReportActionID];
        if (\!fraudAction || fraudAction.actionName \!== fraudActionType) {
            return null;
        }
        // ... rest of logic
    }, [fraudAlertReportActionID, reportActions, fraudActionType]);

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

return {
triggerAmount: message.triggerAmount,
triggerMerchant: message.triggerMerchant,
currency: message.currency ?? CONST.CURRENCY.USD,
Copy link
Contributor

Choose a reason for hiding this comment

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

PERF-13 (docs)

The constant CONST.CURRENCY.USD is accessed inside the useMemo callback. Since this constant is iterator-independent and doesn't depend on the memoized computation, it should be hoisted outside to avoid repeated property lookups.

Suggested fix: Hoist the constant outside the useMemo:

const defaultCurrency = CONST.CURRENCY.USD;
const fraudDetails = useMemo(() => {
    if (\!fraudAlertReportActionID || \!reportActions) {
        return null;
    }

    const fraudAction = reportActions[fraudAlertReportActionID];
    if (\!fraudAction || fraudAction.actionName \!== CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_CARD_FRAUD_ALERT) {
        return null;
    }

    const message = fraudAction.originalMessage as OriginalMessageCardFraudAlert | undefined;
    if (\!message) {
        return null;
    }

    return {
        triggerAmount: message.triggerAmount,
        triggerMerchant: message.triggerMerchant,
        currency: message.currency ?? defaultCurrency,
    };
}, [fraudAlertReportActionID, reportActions, defaultCurrency]);

Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d898d1bc0a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +56 to +58
const handleReviewPress = () => {
if (!fraudAlertReportID) {
return;

Choose a reason for hiding this comment

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

P2 Badge Avoid rendering a dead CTA when report ID is missing

When a card has fraud set but the optional message?.possibleFraud?.fraudAlertReportID isn’t populated (e.g., older data or partial Onyx hydration), the widget still renders while handleReviewPress bails out early. In that scenario the “Review” button is a no‑op, so users can’t reach the fraud report they’re being prompted to review. Consider gating rendering on the report ID, disabling the CTA until it exists, or linking to a fallback destination.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

InternalQA This pull request required internal QA

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants