Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
00ca266
feat: added optimistic report
mananjadhav Sep 21, 2025
6348dfc
refactor: remove type cast
mananjadhav Sep 21, 2025
ec44b76
Merge branch 'main' of github.com:mananjadhav/App into fix/70569-reje…
mananjadhav Sep 22, 2025
ccd0f5f
Merge branch 'main' of github.com:mananjadhav/App into fix/70569-reje…
mananjadhav Sep 24, 2025
9d21088
fix: offline transaction mapping
mananjadhav Sep 24, 2025
e87d772
Merge branch 'main' of github.com:mananjadhav/App into fix/70569-reje…
mananjadhav Sep 27, 2025
385a5ba
Merge branch 'main' of github.com:mananjadhav/App into fix/70569-reje…
mananjadhav Sep 29, 2025
e7565e5
fix:add reportPreviewReportActionID
mananjadhav Sep 30, 2025
00fe9b8
Merge branch 'Expensify:main' into fix/70569-reject-offline-new-report
truph01 Oct 14, 2025
a83dc24
fix: update missing optimistic data
truph01 Oct 14, 2025
e25430a
Merge branch 'Expensify:main' into fix/70569-reject-offline-new-report
truph01 Oct 20, 2025
c1c4b62
fix: lint
truph01 Oct 20, 2025
41dc2cd
Merge branch 'Expensify:main' into fix/70569-reject-offline-new-report
truph01 Oct 22, 2025
6081827
fix: update success and failure data
truph01 Oct 22, 2025
180d7cc
fix: lint
truph01 Oct 22, 2025
9ef3525
Merge branch 'Expensify:main' into fix/70569-reject-offline-new-report
truph01 Oct 28, 2025
e1021fc
fix Selecting hold throws an error
truph01 Oct 28, 2025
bf9359d
fix: incorrect owner account ID
truph01 Oct 28, 2025
cdc5087
fix: conflicts
truph01 Oct 30, 2025
2c98839
fix: add iouAction to rejected report optimistically
truph01 Oct 30, 2025
6667e9d
fix: conflicts
truph01 Oct 31, 2025
7705a44
Merge branch 'Expensify:main' into fix/70569-reject-offline-new-report
truph01 Nov 3, 2025
8a6bb85
fix: empty report is shown after reject
truph01 Nov 3, 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
4 changes: 4 additions & 0 deletions src/libs/API/parameters/RejectMoneyRequestParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ type RejectMoneyRequestParams = {
reportID: string;
comment: string;
rejectedToReportID?: string;
reportPreviewReportActionID?: string;
rejectedActionReportActionID: string;
rejectedCommentReportActionID: string;
createdIOUReportActionID?: string;
expenseMovedReportActionID?: string;
expenseCreatedReportActionID?: string;
};

export default RejectMoneyRequestParams;
255 changes: 234 additions & 21 deletions src/libs/actions/IOU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
import {getCustomUnitID} from '@libs/PerDiemRequestUtils';
import Performance from '@libs/Performance';
import Permissions from '@libs/Permissions';
import {getAccountIDsByLogins} from '@libs/PersonalDetailsUtils';
import {getAccountIDsByLogins, getLoginByAccountID} from '@libs/PersonalDetailsUtils';
import {addSMSDomainIfPhoneNumber} from '@libs/PhoneNumber';
import {
getCorrectedAutoReportingFrequency,
Expand Down Expand Up @@ -696,7 +696,7 @@
};

let allPersonalDetails: OnyxTypes.PersonalDetailsList = {};
Onyx.connect({

Check warning on line 699 in src/libs/actions/IOU.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) => {
allPersonalDetails = value ?? {};
Expand Down Expand Up @@ -773,13 +773,13 @@
};

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

Check warning on line 776 in src/libs/actions/IOU.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),
});

let allTransactions: NonNullable<OnyxCollection<OnyxTypes.Transaction>> = {};
Onyx.connect({

Check warning on line 782 in src/libs/actions/IOU.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,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -793,7 +793,7 @@
});

let allTransactionDrafts: NonNullable<OnyxCollection<OnyxTypes.Transaction>> = {};
Onyx.connect({

Check warning on line 796 in src/libs/actions/IOU.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_DRAFT,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -802,7 +802,7 @@
});

let allTransactionViolations: NonNullable<OnyxCollection<OnyxTypes.TransactionViolations>> = {};
Onyx.connect({

Check warning on line 805 in src/libs/actions/IOU.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 @@ -816,7 +816,7 @@
});

let allNextSteps: NonNullable<OnyxCollection<OnyxTypes.ReportNextStep>> = {};
Onyx.connect({

Check warning on line 819 in src/libs/actions/IOU.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.NEXT_STEP,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -825,14 +825,14 @@
});

let allPolicyCategories: OnyxCollection<OnyxTypes.PolicyCategories> = {};
Onyx.connect({

Check warning on line 828 in src/libs/actions/IOU.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_CATEGORIES,
waitForCollectionCallback: true,
callback: (val) => (allPolicyCategories = val),
});

const allPolicies: OnyxCollection<OnyxTypes.Policy> = {};
Onyx.connect({

Check warning on line 835 in src/libs/actions/IOU.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,
callback: (val, key) => {
if (!key) {
Expand Down Expand Up @@ -868,14 +868,14 @@
// `allRecentlyUsedTags` was moved here temporarily from `src/libs/actions/Policy/Tag.ts` during the `Deprecate Onyx.connect` refactor.
// All uses of this variable should be replaced with `useOnyx`.
let allRecentlyUsedTags: OnyxCollection<RecentlyUsedTags> = {};
Onyx.connect({

Check warning on line 871 in src/libs/actions/IOU.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_RECENTLY_USED_TAGS,
waitForCollectionCallback: true,
callback: (val) => (allRecentlyUsedTags = val),
});

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

Check warning on line 878 in src/libs/actions/IOU.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 @@ -12741,6 +12741,7 @@
const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
const transactionAmount = getAmount(transaction);
const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`];
const policyExpenseChat = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.chatReportID}`];
const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`];
const isPolicyDelayedSubmissionEnabled = policy ? isDelayedSubmissionEnabled(policy) : false;
const isIOU = isIOUReport(report);
Expand All @@ -12755,10 +12756,15 @@

const reportAction = getIOUActionForReportID(reportID, transactionID);
const childReportID = reportAction?.childReportID;
const transactionThreadReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${childReportID}`];

let movedToReport;
let rejectedToReportID;
let urlToNavigateBack;
let reportPreviewAction: OnyxTypes.ReportAction | undefined;
let createdIOUReportActionID;
let expenseMovedReportActionID;
let expenseCreatedReportActionID;

const hasMultipleExpenses = getReportTransactions(reportID).length > 1;

Expand All @@ -12770,6 +12776,7 @@
const baseTimestamp = DateUtils.getDBTime();
const optimisticRejectReportAction = buildOptimisticRejectReportAction(baseTimestamp);
const optimisticRejectReportActionComment = buildOptimisticRejectReportActionComment(comment, DateUtils.addMillisecondsFromDateTime(baseTimestamp, 1));
let movedTransactionAction;

// Build successData and failureData to prevent duplication
const successData: OnyxUpdate[] = [];
Expand Down Expand Up @@ -12938,33 +12945,234 @@
if (existingOpenReport) {
movedToReport = existingOpenReport;
rejectedToReportID = existingOpenReport.reportID;
optimisticData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${movedToReport?.reportID}`,
value: {
...movedToReport,
total: (movedToReport?.total ?? 0) - transactionAmount,
},

const [, , iouAction, ,] = buildOptimisticMoneyRequestEntities({
Copy link
Contributor

@hoangzinh hoangzinh Jan 20, 2026

Choose a reason for hiding this comment

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

We need to set createdIOUReportActionID with reportActionID of the iouAction here as well, otherwise it will cause FE sends empty value for createdIOUReportActionID, hence BE re-generate another createdIOUReportActionID, then cause duplicate report preview. More details here #76982 (comment)

iouReport: movedToReport,
type: CONST.IOU.REPORT_ACTION_TYPE.CREATE,
amount: transactionAmount,
currency: getCurrency(transaction),
comment,
payeeEmail: getLoginByAccountID(report.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID) ?? '',
participants: [{accountID: report?.ownerAccountID}],
transactionID: transaction.transactionID,
existingTransactionThreadReportID: childReportID,
shouldGenerateTransactionThreadReport: false,
});

optimisticData.push(
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${movedToReport?.reportID}`,
value: {
...movedToReport,
total: (movedToReport?.total ?? 0) - transactionAmount,
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${rejectedToReportID}`,
value: {[iouAction.reportActionID]: iouAction},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${childReportID}`,
value: {
parentReportActionID: iouAction.reportActionID,
parentReportID: rejectedToReportID,
},
},
);

// Add success data for existing report update
successData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${movedToReport?.reportID}`,
value: {pendingFields: {total: null}},
});
successData.push(
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${movedToReport?.reportID}`,
value: {pendingFields: {total: null}},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${rejectedToReportID}`,
value: {[iouAction.reportActionID]: {pendingAction: null}},
},
);

// Add failure data to revert existing report total
failureData.push({
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${movedToReport?.reportID}`,
value: {
total: movedToReport?.total ?? 0,
pendingFields: {total: null},
failureData.push(
// Add failure data to revert existing report total
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${movedToReport?.reportID}`,
value: {
total: movedToReport?.total ?? 0,
pendingFields: {total: null},
},
},
});
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${childReportID}`,
value: {
parentReportActionID: transactionThreadReport?.parentReportActionID,
parentReportID: transactionThreadReport?.parentReportID,
},
},
);
} else {
// Create optimistic report for the rejected transaction
rejectedToReportID = generateReportID();
const newExpenseReport = buildOptimisticExpenseReport(
report.chatReportID,
report?.policyID,
report?.ownerAccountID ?? CONST.DEFAULT_NUMBER_ID,
transactionAmount,
getCurrency(transaction),
transactionAmount,
undefined,
rejectedToReportID,
);
const [, createdActionForExpenseReport, iouAction, ,] = buildOptimisticMoneyRequestEntities({
iouReport: newExpenseReport,
type: CONST.IOU.REPORT_ACTION_TYPE.CREATE,
amount: transactionAmount,
currency: getCurrency(transaction),
comment,
payeeEmail: currentUserEmail,
participants: [{accountID: report?.ownerAccountID}],
transactionID: transaction.transactionID,
existingTransactionThreadReportID: childReportID,
shouldGenerateTransactionThreadReport: false,
});

reportPreviewAction = buildOptimisticReportPreview(policyExpenseChat, newExpenseReport, undefined, transaction, undefined);
movedTransactionAction = buildOptimisticMovedTransactionAction(childReportID, newExpenseReport.reportID);
createdIOUReportActionID = iouAction.reportActionID;
expenseMovedReportActionID = movedTransactionAction.reportActionID;
expenseCreatedReportActionID = createdActionForExpenseReport.reportActionID;
newExpenseReport.parentReportActionID = reportPreviewAction.reportActionID;
optimisticData.push(
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${policyExpenseChat?.reportID}`,
value: {
lastVisibleActionCreated: reportPreviewAction.created,
},
},
{
onyxMethod: Onyx.METHOD.SET,
key: `${ONYXKEYS.COLLECTION.REPORT}${rejectedToReportID}`,
value: {
...newExpenseReport,
pendingFields: {createReport: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD},
},
},
{
onyxMethod: Onyx.METHOD.SET,
key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${rejectedToReportID}`,
value: {
isOptimisticReport: true,
hasOnceLoadedReportActions: true,
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${rejectedToReportID}`,
value: {[createdActionForExpenseReport.reportActionID]: createdActionForExpenseReport, [iouAction.reportActionID]: iouAction},
},
{
onyxMethod: Onyx.METHOD.SET,
key: `${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${rejectedToReportID}`,
value: {
parentReportID: report?.chatReportID,
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${policyExpenseChat?.reportID}`,
value: {
[reportPreviewAction.reportActionID]: reportPreviewAction,
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${childReportID}`,
value: {
parentReportActionID: iouAction.reportActionID,
parentReportID: rejectedToReportID,
},
},
);
successData.push(
{
onyxMethod: Onyx.METHOD.SET,
key: `${ONYXKEYS.COLLECTION.REPORT}${rejectedToReportID}`,
value: {
pendingFields: null,
},
},
Comment on lines +13104 to +13110
Copy link
Contributor

Choose a reason for hiding this comment

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

Coming from #78577 checklist: we should update pendingFields using the MERGE method instead of SET the entire object.

{
onyxMethod: Onyx.METHOD.SET,
key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${rejectedToReportID}`,
value: {
isOptimisticReport: null,
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${rejectedToReportID}`,
value: {[createdActionForExpenseReport.reportActionID]: {pendingAction: null}, [iouAction.reportActionID]: {pendingAction: null}},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${policyExpenseChat?.reportID}`,
value: {
[reportPreviewAction.reportActionID]: {pendingAction: null},
},
},
);

failureData.push(
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${policyExpenseChat?.reportID}`,
value: {
lastVisibleActionCreated: policyExpenseChat?.lastVisibleActionCreated,
},
},
{
onyxMethod: Onyx.METHOD.SET,
key: `${ONYXKEYS.COLLECTION.REPORT}${rejectedToReportID}`,
value: null,
},
{
onyxMethod: Onyx.METHOD.SET,
key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${rejectedToReportID}`,
value: null,
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${rejectedToReportID}`,
value: null,
},
{
onyxMethod: Onyx.METHOD.SET,
key: `${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${rejectedToReportID}`,
value: null,
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${policyExpenseChat?.reportID}`,
value: {
[reportPreviewAction.reportActionID]: null,
},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${childReportID}`,
value: {
parentReportActionID: transactionThreadReport?.parentReportActionID,
parentReportID: transactionThreadReport?.parentReportID,
},
},
);
}
optimisticData.push(
{
Expand Down Expand Up @@ -13069,6 +13277,7 @@
value: {
[optimisticRejectReportAction.reportActionID]: optimisticRejectReportAction,
[optimisticRejectReportActionComment.reportActionID]: optimisticRejectReportActionComment,
...(movedTransactionAction ? {[movedTransactionAction.reportActionID]: movedTransactionAction} : {}),
},
});

Expand Down Expand Up @@ -13187,8 +13396,12 @@
reportID,
comment,
rejectedToReportID,
reportPreviewReportActionID: reportPreviewAction?.reportActionID,
rejectedActionReportActionID: optimisticRejectReportAction.reportActionID,
rejectedCommentReportActionID: optimisticRejectReportActionComment.reportActionID,
createdIOUReportActionID,
expenseMovedReportActionID,
expenseCreatedReportActionID,
};

// Make API call
Expand Down
Loading