Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
8291b98
allow remove demo transaction
DylanDylann Jun 23, 2025
29b4cc7
merge main
DylanDylann Jun 26, 2025
4915983
new update
DylanDylann Jun 26, 2025
3ac15e6
fix UTs
DylanDylann Jun 26, 2025
26d13ce
fix lint
DylanDylann Jun 26, 2025
61fe4f8
lint fix
DylanDylann Jun 26, 2025
3b09e51
lint fix
DylanDylann Jun 26, 2025
0036d68
Merge branch 'main' into allow-remove-demo-transaction
DylanDylann Jun 30, 2025
f2e6363
fix UTs
DylanDylann Jun 30, 2025
a8bfe26
add UTs
DylanDylann Jun 30, 2025
d1b4957
Merge branch 'main' into allow-remove-demo-transaction
DylanDylann Jun 30, 2025
c84fdd8
Merge branch 'main' into allow-remove-demo-transaction
DylanDylann Jul 3, 2025
4f08b0e
add optimistic data
DylanDylann Jul 3, 2025
eef1756
add optimistic data
DylanDylann Jul 3, 2025
0a45574
merge main
DylanDylann Jul 3, 2025
47fdae7
merge main
DylanDylann Jul 3, 2025
c8cbac0
lint fix
DylanDylann Jul 3, 2025
3650fb2
lint fix
DylanDylann Jul 3, 2025
8a1ec58
update comment
DylanDylann Jul 4, 2025
d75b213
type fix
DylanDylann Jul 4, 2025
0bc1904
update test
DylanDylann Jul 4, 2025
161e7c8
chore: update comment
DylanDylann Jul 4, 2025
e66b241
remove redundant
DylanDylann Jul 5, 2025
a7d0c47
don't strike out the svg component
DylanDylann Jul 7, 2025
5ce6026
update optimistic data
DylanDylann Jul 7, 2025
dc03f68
add opt data
DylanDylann Jul 7, 2025
da375b1
remove import
DylanDylann Jul 7, 2025
058a264
Update opt data
DylanDylann Jul 7, 2025
f060c5b
resolve cmt
DylanDylann Jul 7, 2025
5786770
Merge branch 'main' into allow-remove-demo-transaction
DylanDylann Jul 8, 2025
6817a4f
merge main
DylanDylann Jul 10, 2025
acaeff2
merge main
DylanDylann Jul 10, 2025
68309d4
Merge branch 'main' into allow-remove-demo-transaction
DylanDylann Jul 13, 2025
d996a2d
chore
DylanDylann Jul 13, 2025
d57586b
chore
DylanDylann Jul 13, 2025
b632a7c
Merge branch 'main' into allow-remove-demo-transaction
DylanDylann Jul 15, 2025
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
3 changes: 2 additions & 1 deletion src/components/OfflineWithFeedback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type ChildrenProps from '@src/types/utils/ChildrenProps';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import CustomStylesForChildrenProvider from './CustomStylesForChildrenProvider';
import ErrorMessageRow from './ErrorMessageRow';
import ImageSVG from './ImageSVG';

/**
* This component should be used when we are using the offline pattern B (offline with feedback).
Expand Down Expand Up @@ -108,7 +109,7 @@ function OfflineWithFeedback({
const applyStrikeThrough = useCallback(
(childrenProp: React.ReactNode): React.ReactNode => {
const strikeThroughChildren = mapChildrenFlat(childrenProp, (child) => {
if (!React.isValidElement(child)) {
if (!React.isValidElement(child) || child.type === ImageSVG) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This change to fix this crash app mentioned here

When we remove the paid expense offline, we shoulnd't strike out the paid icon because it is svg component

Screenshot 2025-07-07 at 14 33 12

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we should look into this more and fix the root cause as I don't think this is explicitly linked to SVG component

return child;
}

Expand Down
2 changes: 2 additions & 0 deletions src/libs/DebugUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@
const TRANSACTION_VIOLATION_REQUIRED_PROPERTIES: Array<keyof TransactionViolation> = ['type', 'name'] satisfies Array<keyof TransactionViolation>;

let isInFocusMode: OnyxEntry<boolean>;
Onyx.connect({

Check warning on line 91 in src/libs/DebugUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.NVP_PRIORITY_MODE,
callback: (priorityMode) => {
isInFocusMode = priorityMode === CONST.PRIORITY_MODE.GSD;
Expand All @@ -96,7 +96,7 @@
});

let transactionViolations: OnyxCollection<TransactionViolation[]>;
Onyx.connect({

Check warning on line 99 in src/libs/DebugUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -105,7 +105,7 @@
});

let betas: OnyxEntry<Beta[]>;
Onyx.connect({

Check warning on line 108 in src/libs/DebugUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.BETAS,
callback: (value) => {
betas = value;
Expand Down Expand Up @@ -1069,6 +1069,7 @@
inserted: CONST.RED_BRICK_ROAD_PENDING_ACTION,
accountant: CONST.RED_BRICK_ROAD_PENDING_ACTION,
splitExpenses: CONST.RED_BRICK_ROAD_PENDING_ACTION,
isDemoTransaction: CONST.RED_BRICK_ROAD_PENDING_ACTION,
},
'string',
);
Expand Down Expand Up @@ -1107,6 +1108,7 @@
splits: 'array',
dismissedViolations: 'object',
splitExpenses: 'array',
isDemoTransaction: 'boolean',
});
case 'accountant':
return validateObject<ObjectElement<Transaction, 'accountant'>>(value, {
Expand Down
5 changes: 5 additions & 0 deletions src/libs/ReportSecondaryActionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
getOriginalTransactionWithSplitInfo,
hasReceipt as hasReceiptTransactionUtils,
isCardTransaction as isCardTransactionUtils,
isDemoTransaction,
isDuplicate,
isOnHold as isOnHoldTransactionUtils,
isPending,
Expand Down Expand Up @@ -425,6 +426,10 @@ function isDeleteAction(report: Report, reportTransactions: Transaction[], repor
const isSingleTransaction = reportTransactions.length === 1;
const isInvoiceReport = isInvoiceReportUtils(report);

if (reportTransactions.length > 0 && reportTransactions.every((t) => isDemoTransaction(t))) {
return true;
}

if (isUnreported) {
return isOwner;
}
Expand Down
13 changes: 10 additions & 3 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@
hasViolation,
hasWarningTypeViolation,
isCardTransaction as isCardTransactionTransactionUtils,
isDemoTransaction,
isDistanceRequest,
isExpensifyCardTransaction,
isFetchingWaypointsFromServer,
Expand Down Expand Up @@ -372,6 +373,7 @@
created?: string;
linkedExpenseReportAction?: OnyxEntry<ReportAction>;
isPersonalTrackingExpense?: boolean;
reportActionID?: string;
};

type OptimisticIOUReportAction = Pick<
Expand Down Expand Up @@ -883,7 +885,7 @@
const parsedReportActionMessageCache: Record<string, string> = {};

let conciergeReportID: OnyxEntry<string>;
Onyx.connect({

Check warning on line 888 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.CONCIERGE_REPORT_ID,
callback: (value) => {
conciergeReportID = value;
Expand All @@ -891,7 +893,7 @@
});

const defaultAvatarBuildingIconTestID = 'SvgDefaultAvatarBuilding Icon';
Onyx.connect({

Check warning on line 896 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.SESSION,
callback: (value) => {
// When signed out, val is undefined
Expand All @@ -909,7 +911,7 @@
let allPersonalDetails: OnyxEntry<PersonalDetailsList>;
let allPersonalDetailLogins: string[];
let currentUserPersonalDetails: OnyxEntry<PersonalDetails>;
Onyx.connect({

Check warning on line 914 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
callback: (value) => {
if (currentUserAccountID) {
Expand All @@ -921,14 +923,14 @@
});

let allReportsDraft: OnyxCollection<Report>;
Onyx.connect({

Check warning on line 926 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT_DRAFT,
waitForCollectionCallback: true,
callback: (value) => (allReportsDraft = value),
});

let allPolicies: OnyxCollection<Policy>;
Onyx.connect({

Check warning on line 933 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.POLICY,
waitForCollectionCallback: true,
callback: (value) => (allPolicies = value),
Expand All @@ -936,7 +938,7 @@

let allReports: OnyxCollection<Report>;
let reportsByPolicyID: ReportByPolicyMap;
Onyx.connect({

Check warning on line 941 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT,
waitForCollectionCallback: true,
callback: (value) => {
Expand Down Expand Up @@ -977,7 +979,7 @@
});

let allBetas: OnyxEntry<Beta[]>;
Onyx.connect({

Check warning on line 982 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.BETAS,
callback: (value) => (allBetas = value),
});
Expand Down Expand Up @@ -2500,13 +2502,17 @@
* Can only delete if the author is this user and the action is an ADD_COMMENT action or an IOU action in an unsettled report, or if the user is a
* policy admin
*/
function canDeleteReportAction(reportAction: OnyxInputOrEntry<ReportAction>, reportID: string | undefined, iouTransaction?: OnyxEntry<Transaction>): boolean {
function canDeleteReportAction(reportAction: OnyxInputOrEntry<ReportAction>, reportID: string | undefined, transaction: OnyxEntry<Transaction> | undefined): boolean {
const report = getReportOrDraftReport(reportID);
const isActionOwner = reportAction?.actorAccountID === currentUserAccountID;
const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`] ?? null;

if (isDemoTransaction(transaction)) {
return true;
}

if (isMoneyRequestAction(reportAction)) {
const isCardTransactionCanBeDeleted = canDeleteCardTransactionByLiabilityType(iouTransaction);
const isCardTransactionCanBeDeleted = canDeleteCardTransactionByLiabilityType(transaction);
// For now, users cannot delete split actions
const isSplitAction = getOriginalMessage(reportAction)?.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT;

Expand Down Expand Up @@ -6247,6 +6253,7 @@
created = DateUtils.getDBTime(),
linkedExpenseReportAction,
isPersonalTrackingExpense = false,
reportActionID,
} = params;

const IOUReportID = isPersonalTrackingExpense ? undefined : iouReportID || generateReportID();
Expand Down Expand Up @@ -6306,7 +6313,7 @@
automatic: false,
isAttachmentOnly: false,
originalMessage,
reportActionID: rand64(),
reportActionID: reportActionID ?? rand64(),
shouldShow: true,
created,
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
Expand Down
11 changes: 10 additions & 1 deletion src/libs/TransactionUtils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ type BuildOptimisticTransactionParams = {
existingTransaction?: OnyxEntry<Transaction>;
policy?: OnyxEntry<Policy>;
transactionParams: TransactionParams;
isDemoTransactionParam?: boolean;
};

let allTransactions: OnyxCollection<Transaction> = {};
Expand Down Expand Up @@ -247,7 +248,7 @@ function isPendingCardOrScanningTransaction(transaction: OnyxEntry<Transaction>)
* it's transactionID match what was already generated.
*/
function buildOptimisticTransaction(params: BuildOptimisticTransactionParams): Transaction {
const {originalTransactionID = '', existingTransactionID, existingTransaction, policy, transactionParams} = params;
const {originalTransactionID = '', existingTransactionID, existingTransaction, policy, transactionParams, isDemoTransactionParam} = params;
const {
amount,
currency,
Expand Down Expand Up @@ -275,6 +276,9 @@ function buildOptimisticTransaction(params: BuildOptimisticTransactionParams): T
const transactionID = existingTransactionID ?? rand64();

const commentJSON: Comment = {comment, attendees};
if (isDemoTransactionParam) {
commentJSON.isDemoTransaction = true;
}
if (source) {
commentJSON.source = source;
}
Expand Down Expand Up @@ -337,6 +341,10 @@ function hasReceiptSource(transaction: OnyxInputOrEntry<Transaction>): boolean {
return !!transaction?.receipt?.source;
}

function isDemoTransaction(transaction: OnyxInputOrEntry<Transaction>): boolean {
return transaction?.comment?.isDemoTransaction ?? false;
}

function isMerchantMissing(transaction: OnyxEntry<Transaction>) {
if (transaction?.modifiedMerchant && transaction.modifiedMerchant !== '') {
return transaction.modifiedMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT;
Expand Down Expand Up @@ -1801,6 +1809,7 @@ export {
getTransactionPendingAction,
isTransactionPendingDelete,
createUnreportedExpenseSections,
isDemoTransaction,
shouldShowViolation,
isUnreportedAndHasInvalidDistanceRateTransaction,
};
Expand Down
3 changes: 3 additions & 0 deletions src/libs/actions/IOU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1345,6 +1345,7 @@ function buildOnyxDataForTestDriveIOU(testDriveIOUParams: BuildOnyxDataForTestDr
paymentType: CONST.IOU.PAYMENT_TYPE.ELSEWHERE,
iouReportID: testDriveIOUParams.iouOptimisticParams.report.reportID,
transactionID: testDriveIOUParams.transaction.transactionID,
reportActionID: testDriveIOUParams.iouOptimisticParams.action.reportActionID,
});

const text = Localize.translateLocal('testDrive.employeeInviteMessage', {name: personalDetailsList?.[userAccountID]?.firstName ?? ''});
Expand Down Expand Up @@ -1605,6 +1606,7 @@ function buildOnyxDataForMoneyRequest(moneyRequestParams: BuildOnyxDataForMoneyR
paymentType: isScanRequest && !isTestReceipt ? undefined : CONST.IOU.PAYMENT_TYPE.ELSEWHERE,
iouReportID: iou.report.reportID,
transactionID: transaction.transactionID,
reportActionID: iou.action.reportActionID,
});

optimisticData.push(
Expand Down Expand Up @@ -3387,6 +3389,7 @@ function getMoneyRequestInformation(moneyRequestInformation: MoneyRequestInforma
billable,
pendingFields: isDistanceRequest ? {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD} : undefined,
},
isDemoTransactionParam: isSelectedManagerMcTest(participant.login) || transactionParams.receipt?.isTestDriveReceipt,
});

const optimisticPolicyRecentlyUsedCategories = buildOptimisticPolicyRecentlyUsedCategories(iouReport.policyID, category);
Expand Down
5 changes: 3 additions & 2 deletions src/pages/ReportDetailsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ import {
shouldUseFullTitleToDisplay,
} from '@libs/ReportUtils';
import StringUtils from '@libs/StringUtils';
import {isDemoTransaction} from '@libs/TransactionUtils';
import {deleteMoneyRequest, deleteTrackExpense, getNavigationUrlAfterTrackExpenseDelete, getNavigationUrlOnMoneyRequestDelete} from '@userActions/IOU';
import {
clearAvatarErrors,
Expand Down Expand Up @@ -245,7 +246,6 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail
});

const isPrivateNotesFetchTriggered = reportMetadata?.isLoadingPrivateNotes !== undefined;

const requestParentReportAction = useMemo(() => {
// 2. MoneyReport case
if (caseID === CASES.MONEY_REPORT) {
Expand Down Expand Up @@ -281,7 +281,8 @@ function ReportDetailsPage({policy, report, route, reportMetadata}: ReportDetail
const iouTransactionID = isMoneyRequestAction(requestParentReportAction) ? getOriginalMessage(requestParentReportAction)?.IOUTransactionID : undefined;
const [iouTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION}${iouTransactionID}`, {canBeMissing: true});
const isCardTransactionCanBeDeleted = canDeleteCardTransactionByLiabilityType(iouTransaction);
const shouldShowDeleteButton = shouldShowTaskDeleteButton || (canDeleteRequest && isCardTransactionCanBeDeleted);
const shouldShowDeleteButton = shouldShowTaskDeleteButton || (canDeleteRequest && isCardTransactionCanBeDeleted) || isDemoTransaction(iouTransaction);

useEffect(() => {
if (canDeleteRequest) {
return;
Expand Down
3 changes: 3 additions & 0 deletions src/types/onyx/Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ type Comment = {
/** Whether the transaction comment is loading */
isLoading?: boolean;

/** Whether the transaction comment is a demo transaction */
isDemoTransaction?: boolean;

/** Type of the transaction */
type?: ValueOf<typeof CONST.TRANSACTION.TYPE>;

Expand Down
3 changes: 2 additions & 1 deletion tests/perf-test/ReportUtils.perf-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,11 @@ describe('ReportUtils', () => {

test('[ReportUtils] canDeleteReportAction on 1k reports and policies', async () => {
const reportID = '1';
const transaction = createRandomTransaction(1);
const reportAction = {...createRandomReportAction(1), actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT} as unknown as ReportAction;

await waitForBatchedUpdates();
await measureFunction(() => canDeleteReportAction(reportAction, reportID));
await measureFunction(() => canDeleteReportAction(reportAction, reportID, transaction));
});

test('[ReportUtils] getReportRecipientAccountID on 1k participants', async () => {
Expand Down
32 changes: 32 additions & 0 deletions tests/unit/ReportSecondaryActionUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1069,6 +1069,38 @@ describe('getSecondaryAction', () => {
expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.DELETE)).toBe(false);
});

it('include DELETE option for demo transaction', async () => {
const report = {
reportID: REPORT_ID,
type: CONST.REPORT.TYPE.EXPENSE,
ownerAccountID: EMPLOYEE_ACCOUNT_ID,
statusNum: CONST.REPORT.STATUS_NUM.SUBMITTED,
stateNum: CONST.REPORT.STATE_NUM.SUBMITTED,
} as unknown as Report;

const TRANSACTION_ID = 'TRANSACTION_ID';

const transaction = {
transactionID: TRANSACTION_ID,
reportID: REPORT_ID,
comment: {
isDemoTransaction: true,
},
} as unknown as Transaction;

const policy = {
id: POLICY_ID,
approvalMode: CONST.POLICY.APPROVAL_MODE.BASIC,
approver: APPROVER_EMAIL,
} as unknown as Policy;

await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, report);
await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, policy);

const result = getSecondaryReportActions({report, chatReport, reportTransactions: [transaction], violations: {}, policy});
expect(result.includes(CONST.REPORT.SECONDARY_ACTIONS.DELETE)).toBe(true);
});

it('include EXPORT_TO_ACCOUNTING option for settled expense report', async () => {
const report = {
reportID: REPORT_ID,
Expand Down
44 changes: 44 additions & 0 deletions tests/unit/ReportUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2719,6 +2719,50 @@ describe('ReportUtils', () => {

expect(canDeleteReportAction(moneyRequestAction, currentReportId, transaction)).toBe(false);
});

it('should return true for demo transaction', () => {
const transaction = {
...createRandomTransaction(1),
comment: {
isDemoTransaction: true,
},
};

const report = LHNTestUtils.getFakeReport();
const parentReportAction: ReportAction = {
...LHNTestUtils.getFakeReportAction(),
message: [
{
type: 'COMMENT',
html: 'hey',
text: 'hey',
isEdited: false,
whisperedTo: [],
isDeletedParentAction: false,
moderationDecision: {
decision: CONST.MODERATION.MODERATOR_DECISION_PENDING_REMOVE,
},
},
],
childReportID: report.reportID,
};
const moneyRequestAction = {
...parentReportAction,
actorAccountID: currentUserAccountID,
actionName: CONST.REPORT.ACTIONS.TYPE.IOU,
originalMessage: {
IOUReportID: '1',
IOUTransactionID: '1',
amount: 100,
participantAccountID: 1,
currency: CONST.CURRENCY.USD,
type: CONST.IOU.REPORT_ACTION_TYPE.PAY,
paymentType: CONST.IOU.PAYMENT_TYPE.EXPENSIFY,
},
};

expect(canDeleteReportAction(moneyRequestAction, '1', transaction)).toBe(true);
});
});

describe('getPolicyExpenseChat', () => {
Expand Down
Loading