Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
5e23e5c
Fixed the condition to check fraud alert action on home screen in tim…
Uzaifm127 Apr 12, 2026
86364cc
added possible fraud to null in optimistic data and rolling back to p…
Uzaifm127 Apr 13, 2026
264fd2a
added unresolved ACTIONABLE_CARD_FRAUD_ALERT report action in useTime…
Uzaifm127 Apr 13, 2026
1a290fe
Merge branch 'main' of https://github.com/Uzaifm127/App into fix/85981
Uzaifm127 Apr 13, 2026
790f48f
subscribed to report actions in useTimeSensitiveCards to fix fraud ca…
Uzaifm127 Apr 14, 2026
8850fc4
Fixed conflicts: resolve fraud alert merge conflicts by routing possi…
Uzaifm127 Apr 16, 2026
5a31eca
fixed conflicts: added isCardWithCustomZeroLimit import
Uzaifm127 Apr 18, 2026
6aa046d
added unresolved fraud report action for custom -zsh limit card in us…
Uzaifm127 Apr 18, 2026
b14aa8b
fixed conflicts
Uzaifm127 Apr 21, 2026
67dfd4c
fix conflicts: Removed ExpensifyCardRuleFilter import
Uzaifm127 Apr 24, 2026
465cd7f
fixed the failing ts and eslint check by looking the fraud alert poss…
Uzaifm127 Apr 25, 2026
8eb56a4
refined the logic for report actions subscription for fraud card tasks
Uzaifm127 Apr 25, 2026
b8361d3
added the test case for resolved fraud alert actions hiding stale Hom…
Uzaifm127 Apr 27, 2026
d20bcfd
Merge branch 'main' of https://github.com/Uzaifm127/App into fix/85981
Uzaifm127 Apr 27, 2026
e1a4785
fixed eslint: Removed baseFraudAction.originalMessage
Uzaifm127 Apr 27, 2026
d516729
fixed conflicts: added PersonalDetailsList import
Uzaifm127 May 1, 2026
c742de6
Merge branch 'main' of https://github.com/Uzaifm127/App into fix/85981
Uzaifm127 May 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4166,9 +4166,9 @@ type ReasonAndReportActionThatRequiresAttention = {
/**
* Returns the unresolved card fraud alert action for a given report.
*/
function getUnresolvedCardFraudAlertAction(reportID: string): OnyxEntry<ReportAction> {
const reportActions = getAllReportActions(reportID);
return Object.values(reportActions).find((action): action is ReportAction => isActionableCardFraudAlert(action) && !getOriginalMessage(action)?.resolution);
function getUnresolvedCardFraudAlertAction(reportID: string, reportActions?: OnyxEntry<ReportActions>): OnyxEntry<ReportAction> {
const actions = reportActions ?? getAllReportActions(reportID);
return Object.values(actions).find((action): action is ReportAction => isActionableCardFraudAlert(action) && !getOriginalMessage(action)?.resolution);
}

/**
Expand Down Expand Up @@ -13647,6 +13647,7 @@ export {
hasMissingInvoiceBankAccount,
reasonForReportToBeInOptionList,
getReasonAndReportActionThatRequiresAttention,
getUnresolvedCardFraudAlertAction,
buildOptimisticChangeFieldAction,
isPolicyRelatedReport,
hasReportErrorsOtherThanFailedReceipt,
Expand Down
36 changes: 32 additions & 4 deletions src/libs/actions/Card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {SpendRuleForm} from '@src/types/form';
import type {Card, CompanyCardFeedWithDomainID, PersonalDetailsList, Report, Transaction} from '@src/types/onyx';
import type {CardLimitType, ExpensifyCardDetails, IssueNewCardData, IssueNewCardStep} from '@src/types/onyx/Card';
import type {CardLimitType, ExpensifyCardDetails, IssueNewCardData, IssueNewCardStep, PossibleFraudData} from '@src/types/onyx/Card';
import type {ExpensifyCardRule} from '@src/types/onyx/ExpensifyCardSettings';
import type {SelectedTimezone} from '@src/types/onyx/PersonalDetails';
import type {ConnectionName} from '@src/types/onyx/Policy';
Expand Down Expand Up @@ -1749,15 +1749,21 @@ function deleteExpensifyCardRule(domainAccountID: number, cardRuleID: string, ex
* Resolves a fraud alert for a given card.
* When the user clicks on the whisper it sets the optimistic data to the resolution and calls the API
*/
function resolveFraudAlert(cardID: number | undefined, isFraud: boolean, reportID: string | undefined, reportActionID: string | undefined) {
function resolveFraudAlert(
cardID: number | undefined,
isFraud: boolean,
reportID: string | undefined,
reportActionID: string | undefined,
previousPossibleFraud: PossibleFraudData | null = null,
) {
if (!reportID || !reportActionID || !cardID) {
Log.hmmm('[resolveFraudAlert] Missing required parameters');
return;
}

const resolution = isFraud ? CONST.CARD_FRAUD_ALERT_RESOLUTION.FRAUD : CONST.CARD_FRAUD_ALERT_RESOLUTION.RECOGNIZED;

const optimisticData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS>> = [
const optimisticData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS | typeof ONYXKEYS.CARD_LIST>> = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
Expand All @@ -1770,6 +1776,17 @@ function resolveFraudAlert(cardID: number | undefined, isFraud: boolean, reportI
},
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.CARD_LIST,
value: {
[cardID]: {
nameValuePairs: {
possibleFraud: null,
},
},
},
},
];

const successData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS>> = [
Expand All @@ -1784,7 +1801,7 @@ function resolveFraudAlert(cardID: number | undefined, isFraud: boolean, reportI
},
];

const failureData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS>> = [
const failureData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS | typeof ONYXKEYS.CARD_LIST>> = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
Expand All @@ -1798,6 +1815,17 @@ function resolveFraudAlert(cardID: number | undefined, isFraud: boolean, reportI
},
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: ONYXKEYS.CARD_LIST,
value: {
[cardID]: {
nameValuePairs: {
possibleFraud: previousPossibleFraud,
},
},
},
},
];

const parameters: ResolveFraudAlertParams = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import useOnyx from '@hooks/useOnyx';
import {isCard, isCardPendingActivate, isCardPendingIssue, isCardWithCustomZeroLimit, isCardWithPotentialFraud, isExpensifyCard} from '@libs/CardUtils';
import {getUnresolvedCardFraudAlertAction} from '@libs/ReportUtils';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Card} from '@src/types/onyx';

function useTimeSensitiveCards() {
const [cards] = useOnyx(ONYXKEYS.CARD_LIST);
const [allReportActions] = useOnyx(ONYXKEYS.COLLECTION.REPORT_ACTIONS);

const cardsNeedingShippingAddress: Card[] = [];
const cardsNeedingActivation: Card[] = [];
Expand All @@ -19,7 +21,11 @@ function useTimeSensitiveCards() {
continue;
}

if (isCardWithPotentialFraud(card) && card.nameValuePairs?.possibleFraud?.fraudAlertReportID) {
const fraudAlertReportID = card.nameValuePairs?.possibleFraud?.fraudAlertReportID;
const reportActions = fraudAlertReportID ? allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${fraudAlertReportID}`] : undefined;
const hasUnresolvedFraudAction = !!fraudAlertReportID && !!getUnresolvedCardFraudAlertAction(String(fraudAlertReportID), reportActions);

if (isCardWithPotentialFraud(card) && !!fraudAlertReportID && hasUnresolvedFraudAction) {
cardsWithFraud.push(card);
}

Expand Down
9 changes: 7 additions & 2 deletions src/pages/inbox/report/actionContents/FraudAlertContent.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import {cardByIdSelector} from '@selectors/Card';
import React from 'react';
import {View} from 'react-native';
import type {ActionableItem} from '@components/ReportActionItem/ActionableItemButtons';
import ActionableItemButtons from '@components/ReportActionItem/ActionableItemButtons';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import {getActionableCardFraudAlertMessage, getOriginalMessage} from '@libs/ReportActionsUtils';
import ReportActionItemBasicMessage from '@pages/inbox/report/ReportActionItemBasicMessage';
import {resolveFraudAlert} from '@userActions/Card';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {ReportAction} from '@src/types/onyx';

type FraudAlertContentProps = {
Expand All @@ -20,6 +23,8 @@ function FraudAlertContent({action, reportID}: FraudAlertContentProps) {
const reportActionID = action?.reportActionID;
const originalMessage = getOriginalMessage(action);
const cardID = originalMessage?.cardID;
const [card] = useOnyx(ONYXKEYS.CARD_LIST, {selector: cardByIdSelector(String(cardID))});
const possibleFraud = card?.nameValuePairs?.possibleFraud ?? null;

const buttons: ActionableItem[] = originalMessage?.resolution
? []
Expand All @@ -28,15 +33,15 @@ function FraudAlertContent({action, reportID}: FraudAlertContentProps) {
text: 'cardPage.cardFraudAlert.confirmButtonText',
key: `${action.reportActionID}-cardFraudAlert-confirm`,
onPress: () => {
resolveFraudAlert(cardID, false, reportID, reportActionID);
resolveFraudAlert(cardID, false, reportID, reportActionID, possibleFraud);
},
isPrimary: true,
},
{
text: 'cardPage.cardFraudAlert.reportFraudButtonText',
key: `${action.reportActionID}-cardFraudAlert-reportFraud`,
onPress: () => {
resolveFraudAlert(cardID, true, reportID, reportActionID);
resolveFraudAlert(cardID, true, reportID, reportActionID, possibleFraud);
},
},
];
Expand Down
50 changes: 50 additions & 0 deletions tests/unit/hooks/useTimeSensitiveCards.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Card, CardList} from '@src/types/onyx';
import {createRandomExpensifyCard} from '../../utils/collections/card';
import createRandomReportAction from '../../utils/collections/reportActions';
import waitForBatchedUpdates from '../../utils/waitForBatchedUpdates';

describe('useTimeSensitiveCards', () => {
Expand Down Expand Up @@ -178,8 +179,15 @@ describe('useTimeSensitiveCards', () => {
possibleFraud: {triggerAmount: 5663, triggerMerchant: 'WAL-MART #2366', currency: 'USD', fraudAlertReportID: 123456},
});
const cardList: CardList = {'1': cardWithFraud};
const unresolvedFraudAction = {
...createRandomReportAction(1),
actionName: CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_CARD_FRAUD_ALERT,
};

await Onyx.merge(ONYXKEYS.CARD_LIST, cardList);
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${cardWithFraud.nameValuePairs?.possibleFraud?.fraudAlertReportID}`, {
[unresolvedFraudAction.reportActionID]: unresolvedFraudAction,
});
await waitForBatchedUpdates();

const {result} = renderHook(() => useTimeSensitiveCards());
Expand All @@ -190,6 +198,34 @@ describe('useTimeSensitiveCards', () => {
expect(result.current.shouldShowReviewCardFraud).toBe(true);
});

it('should not show fraud review when card has possibleFraud data but the fraud action is already resolved as recognized', async () => {
const cardWithResolvedFraudAlert = createRandomExpensifyCard(1, {
state: CONST.EXPENSIFY_CARD.STATE.OPEN,
fraud: CONST.EXPENSIFY_CARD.FRAUD_TYPES.DOMAIN,
possibleFraud: {triggerAmount: 5663, triggerMerchant: 'WAL-MART #2366', currency: 'USD', fraudAlertReportID: 123457},
});
const cardList: CardList = {'1': cardWithResolvedFraudAlert};
const baseFraudAction = createRandomReportAction(4);
const resolvedFraudAction = {
...baseFraudAction,
actionName: CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_CARD_FRAUD_ALERT,
originalMessage: {
resolution: CONST.CARD_FRAUD_ALERT_RESOLUTION.RECOGNIZED,
},
};

await Onyx.merge(ONYXKEYS.CARD_LIST, cardList);
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${cardWithResolvedFraudAlert.nameValuePairs?.possibleFraud?.fraudAlertReportID}`, {
[resolvedFraudAction.reportActionID]: resolvedFraudAction,
});
await waitForBatchedUpdates();

const {result} = renderHook(() => useTimeSensitiveCards());

expect(result.current.cardsWithFraud).toHaveLength(0);
expect(result.current.shouldShowReviewCardFraud).toBe(false);
});

it('should not show fraud review for cards with fraud type NONE and no possibleFraud data', async () => {
const cardWithNoFraud = createRandomExpensifyCard(1, {state: CONST.EXPENSIFY_CARD.STATE.OPEN, fraud: CONST.EXPENSIFY_CARD.FRAUD_TYPES.NONE});
const cardList: CardList = {'1': cardWithNoFraud};
Expand All @@ -210,8 +246,15 @@ describe('useTimeSensitiveCards', () => {
possibleFraud: {triggerAmount: 5663, triggerMerchant: 'WAL-MART #2366', currency: 'USD', fraudAlertReportID: 5230242215684213},
});
const cardList: CardList = {'1': cardWithPendingFraudAlert};
const unresolvedFraudAction = {
...createRandomReportAction(2),
actionName: CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_CARD_FRAUD_ALERT,
};

await Onyx.merge(ONYXKEYS.CARD_LIST, cardList);
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${cardWithPendingFraudAlert.nameValuePairs?.possibleFraud?.fraudAlertReportID}`, {
[unresolvedFraudAction.reportActionID]: unresolvedFraudAction,
});
await waitForBatchedUpdates();

const {result} = renderHook(() => useTimeSensitiveCards());
Expand Down Expand Up @@ -283,8 +326,15 @@ describe('useTimeSensitiveCards', () => {
} as Card['nameValuePairs'],
};
const cardList: CardList = {'1': zeroLimitFraudCard};
const unresolvedFraudAction = {
...createRandomReportAction(3),
actionName: CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_CARD_FRAUD_ALERT,
};

await Onyx.merge(ONYXKEYS.CARD_LIST, cardList);
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${zeroLimitFraudCard.nameValuePairs?.possibleFraud?.fraudAlertReportID}`, {
[unresolvedFraudAction.reportActionID]: unresolvedFraudAction,
});
await waitForBatchedUpdates();

const {result} = renderHook(() => useTimeSensitiveCards());
Expand Down
Loading