-
Notifications
You must be signed in to change notification settings - Fork 3.5k
[Home Page] [Release 3] Add card fraud alert #81058
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
This comment has been minimized.
This comment has been minimized.
🦜 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 diffdiff --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 |
src/selectors/Card.ts
Outdated
| } | ||
|
|
||
| // Only consider Expensify cards | ||
| const isExpensifyCard = card.bank === CONST.EXPENSIFY_CARD.BANK; |
There was a problem hiding this comment.
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!
|
@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] |
| 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}`, { |
There was a problem hiding this comment.
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.
src/selectors/Card.ts
Outdated
| } | ||
|
|
||
| // Only consider Expensify cards | ||
| const isExpensifyCard = card.bank === CONST.EXPENSIFY_CARD.BANK; |
There was a problem hiding this comment.
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 => { |
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
There was a problem hiding this 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".
| const handleReviewPress = () => { | ||
| if (!fraudAlertReportID) { | ||
| return; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 👍 / 👎.
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
Type Extensions: Card.ts
Selector Updates: Card.ts
Hook Updates: useTimeSensitiveCards.ts
TimeSensitiveSection Updates
Fixed Issues
$ #79992
PROPOSAL:
Tests
Prerequisites:
Logged in user with an Expensify Card
Test Steps
Offline tests
N/A
QA Steps
Same as steps
// TODO: These must be filled out, or the issue title must include "[No QA]."
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectioncanBeMissingparam foruseOnyxtoggleReportand notonIconClick)src/languages/*files and using the translation methodSTYLE.md) were followedAvatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.ScrollViewcomponent to make it scrollable when more elements are added to the page.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps./** comment above it */thisproperly so there are no scoping issues (i.e. foronClick={this.submit}the methodthis.submitshould be bound tothisin the constructor)thisare necessary to be bound (i.e. avoidthis.submit = this.submit.bind(this);ifthis.submitis never passed to a component event handler likeonClick)Screenshots/Videos
Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari