Skip to content

Commit 87e6d66

Browse files
authored
Merge pull request Expensify#79456 from dukenv0307/fix/73663
Remove Onyx.connect for the key REPORT_DRAFT_COMMENT
2 parents 9ba7019 + 52bdae8 commit 87e6d66

6 files changed

Lines changed: 342 additions & 109 deletions

File tree

src/Expensify.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import {confirmReadyToOpenApp, openApp, updateLastRoute} from './libs/actions/Ap
2727
import {disconnect} from './libs/actions/Delegate';
2828
import * as EmojiPickerAction from './libs/actions/EmojiPickerAction';
2929
import {openReportFromDeepLink} from './libs/actions/Link';
30+
// This lib needs to be imported, but it has nothing to export since all it contains is an Onyx connection
31+
import './libs/actions/replaceOptimisticReportWithActualReport';
3032
import * as Report from './libs/actions/Report';
3133
import {hasAuthToken} from './libs/actions/Session';
3234
import * as User from './libs/actions/User';

src/libs/ReportUtils.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ import {unholdRequest} from './actions/IOU/Hold';
102102
import {isApprover as isApproverUtils} from './actions/Policy/Member';
103103
import {createDraftWorkspace} from './actions/Policy/Policy';
104104
import {hasCreditBankAccount} from './actions/ReimbursementAccount/store';
105-
import {handlePreexistingReport, openUnreportedExpense} from './actions/Report';
105+
import {openUnreportedExpense} from './actions/Report';
106106
import type {GuidedSetupData, TaskForParameters} from './actions/Report';
107107
import {isAnonymousUser as isAnonymousUserSession} from './actions/Session';
108108
import {removeDraftTransactions} from './actions/TransactionEdit';
@@ -1082,8 +1082,6 @@ Onyx.connectWithoutView({
10821082
return acc;
10831083
}
10841084

1085-
handlePreexistingReport(report);
1086-
10871085
// Get all reports, which are the ones that are:
10881086
// - Owned by the same user
10891087
// - Are either open or submitted

src/libs/actions/Report.ts

Lines changed: 1 addition & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ import Log from '@libs/Log';
7878
import {isEmailPublicDomain} from '@libs/LoginUtils';
7979
import {getMovedReportID} from '@libs/ModifiedExpenseMessage';
8080
import type {LinkToOptions} from '@libs/Navigation/helpers/linkTo/types';
81-
import Navigation, {navigationRef} from '@libs/Navigation/Navigation';
81+
import Navigation from '@libs/Navigation/Navigation';
8282
import enhanceParameters from '@libs/Network/enhanceParameters';
8383
import * as NetworkStore from '@libs/Network/NetworkStore';
8484
import NetworkConnection from '@libs/NetworkConnection';
@@ -161,8 +161,6 @@ import {
161161
isGroupChat as isGroupChatReportUtils,
162162
isHiddenForCurrentUser,
163163
isIOUReportUsingReport,
164-
isMoneyRequest,
165-
isMoneyRequestReport,
166164
isOpenExpenseReport,
167165
isProcessingReport,
168166
isReportManuallyReimbursed,
@@ -182,8 +180,6 @@ import type {OnboardingAccounting} from '@src/CONST';
182180
import CONST from '@src/CONST';
183181
import ONYXKEYS from '@src/ONYXKEYS';
184182
import ROUTES from '@src/ROUTES';
185-
import type {Route} from '@src/ROUTES';
186-
import SCREENS from '@src/SCREENS';
187183
import INPUT_IDS from '@src/types/form/NewRoomForm';
188184
import type {
189185
BankAccountList,
@@ -358,13 +354,6 @@ Onyx.connect({
358354
callback: (val) => (introSelected = val),
359355
});
360356

361-
let allReportDraftComments: Record<string, string | undefined> = {};
362-
Onyx.connect({
363-
key: ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT,
364-
waitForCollectionCallback: true,
365-
callback: (value) => (allReportDraftComments = value),
366-
});
367-
368357
let environment: EnvironmentType;
369358
getEnvironment().then((env) => {
370359
environment = env;
@@ -1979,99 +1968,6 @@ function broadcastUserIsLeavingRoom(reportID: string, currentUserAccountID: numb
19791968
Pusher.sendEvent(privateReportChannelName, Pusher.TYPE.USER_IS_LEAVING_ROOM, leavingStatus);
19801969
}
19811970

1982-
function handlePreexistingReport(report: Report) {
1983-
const {reportID, preexistingReportID, parentReportID, parentReportActionID} = report;
1984-
1985-
if (!reportID || !preexistingReportID) {
1986-
return;
1987-
}
1988-
1989-
// Handle cleanup of stale optimistic IOU report and its report preview separately
1990-
if ((isMoneyRequestReport(report) || isMoneyRequest(report)) && parentReportID && parentReportActionID) {
1991-
const parentReportAction = allReportActions?.[parentReportID]?.[parentReportActionID];
1992-
if (parentReportAction?.childReportID === reportID) {
1993-
Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`, {
1994-
[parentReportActionID]: null,
1995-
});
1996-
}
1997-
Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, null);
1998-
return;
1999-
}
2000-
2001-
// eslint-disable-next-line @typescript-eslint/no-deprecated
2002-
InteractionManager.runAfterInteractions(() => {
2003-
// It is possible that we optimistically created a DM/group-DM for a set of users for which a report already exists.
2004-
// In this case, the API will let us know by returning a preexistingReportID.
2005-
// We should clear out the optimistically created report and re-route the user to the preexisting report.
2006-
let callback = () => {
2007-
const existingReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${preexistingReportID}`];
2008-
2009-
Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, null);
2010-
Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${preexistingReportID}`, {
2011-
...report,
2012-
reportID: preexistingReportID,
2013-
preexistingReportID: null,
2014-
// Replacing the existing report's participants to avoid duplicates
2015-
participants: existingReport?.participants ?? report.participants,
2016-
});
2017-
Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, null);
2018-
};
2019-
2020-
if (!navigationRef.isReady()) {
2021-
callback();
2022-
return;
2023-
}
2024-
2025-
// Use Navigation.getActiveRoute() instead of navigationRef.getCurrentRoute()?.path because
2026-
// getCurrentRoute().path can be undefined during first navigation.
2027-
// We still need getCurrentRoute() for params and screen name as getActiveRoute() only returns the path string.
2028-
const activeRoute = Navigation.getActiveRoute();
2029-
const currentRouteInfo = navigationRef.getCurrentRoute();
2030-
const backTo = (currentRouteInfo?.params as {backTo?: Route})?.backTo;
2031-
const screenName = currentRouteInfo?.name;
2032-
2033-
const isOptimisticReportFocused = activeRoute.includes(`/r/${reportID}`);
2034-
2035-
// Fix specific case: https://github.com/Expensify/App/pull/77657#issuecomment-3678696730.
2036-
// When user is editing a money request report (/e/:reportID route) and has
2037-
// an optimistic report in the background that should be replaced with preexisting report
2038-
const isOptimisticReportInBackground = screenName === SCREENS.RIGHT_MODAL.EXPENSE_REPORT && backTo && backTo.includes(`/r/${reportID}`);
2039-
2040-
// Only re-route them if they are still looking at the optimistically created report
2041-
if (isOptimisticReportFocused || isOptimisticReportInBackground) {
2042-
const currCallback = callback;
2043-
callback = () => {
2044-
currCallback();
2045-
if (isOptimisticReportFocused) {
2046-
Navigation.setParams({reportID: preexistingReportID.toString()});
2047-
} else if (isOptimisticReportInBackground) {
2048-
// Navigate to the correct backTo route with the preexisting report ID
2049-
Navigation.navigate(backTo.replace(`/r/${reportID}`, `/r/${preexistingReportID}`) as Route);
2050-
}
2051-
};
2052-
2053-
// The report screen will listen to this event and transfer the draft comment to the existing report
2054-
// This will allow the newest draft comment to be transferred to the existing report
2055-
DeviceEventEmitter.emit(`switchToPreExistingReport_${reportID}`, {
2056-
preexistingReportID,
2057-
callback,
2058-
});
2059-
2060-
return;
2061-
}
2062-
2063-
// In case the user is not on the report screen, we will transfer the report draft comment directly to the existing report
2064-
// after that clear the optimistically created report
2065-
const draftReportComment = allReportDraftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`];
2066-
if (!draftReportComment) {
2067-
callback();
2068-
return;
2069-
}
2070-
2071-
saveReportDraftComment(preexistingReportID, draftReportComment, callback);
2072-
});
2073-
}
2074-
20751971
/** Deletes a comment from the report, basically sets it as empty string */
20761972
function deleteReportComment(
20771973
reportID: string | undefined,
@@ -6633,7 +6529,6 @@ export {
66336529
getNewerActions,
66346530
getOlderActions,
66356531
getReportPrivateNote,
6636-
handlePreexistingReport,
66376532
handleUserDeletedLinksInHtml,
66386533
hasErrorInPrivateNotes,
66396534
inviteToGroupChat,
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
import {DeviceEventEmitter, InteractionManager} from 'react-native';
2+
import type {OnyxCollection} from 'react-native-onyx';
3+
import Onyx from 'react-native-onyx';
4+
import Navigation, {navigationRef} from '@libs/Navigation/Navigation';
5+
import {isMoneyRequest, isMoneyRequestReport} from '@libs/ReportUtils';
6+
import ONYXKEYS from '@src/ONYXKEYS';
7+
import type {Route} from '@src/ROUTES';
8+
import SCREENS from '@src/SCREENS';
9+
import type {Report, ReportActions} from '@src/types/onyx';
10+
import {saveReportDraftComment} from './Report';
11+
12+
/**
13+
* replaceOptimisticReportWithActualReport
14+
*
15+
* This module handles a specific edge case in the Expensify app's offline-first architecture.
16+
*
17+
* THE PROBLEM:
18+
* When a user creates a new DM or group chat, we optimistically create a report with a temporary
19+
* reportID so they can start using it immediately (offline-first UX). However, when the API request
20+
* completes, the server might respond that a report already exists for that set of participants
21+
* (e.g., if the user previously had a DM with that person). In this case, the API returns a
22+
* `preexistingReportID` indicating which report should be used instead of the optimistic one.
23+
*
24+
* THE SOLUTION:
25+
* This module listens to the REPORT collection in Onyx. When a report comes in with a
26+
* `preexistingReportID` field set, it means we need to:
27+
* 1. Delete the optimistically created report (the one with the temporary ID)
28+
* 2. Redirect the user to the preexisting report (if they're currently viewing the optimistic one)
29+
* 3. Transfer any draft comment from the optimistic report to the preexisting report
30+
* 4. Clean up associated data like parent report actions for money request reports
31+
*
32+
*/
33+
34+
let allReportDraftComments: Record<string, string | undefined> = {};
35+
// Draft comments are cached only for transferring to the preexisting report; no UI subscribes, so connectWithoutView() is used.
36+
Onyx.connectWithoutView({
37+
key: ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT,
38+
waitForCollectionCallback: true,
39+
callback: (value) => (allReportDraftComments = value ?? {}),
40+
});
41+
42+
let allReports: OnyxCollection<Report>;
43+
44+
const allReportActions: OnyxCollection<ReportActions> = {};
45+
// Report actions are cached only to resolve parent actions for IOU cleanup; no UI subscribes, so connectWithoutView() is used.
46+
Onyx.connectWithoutView({
47+
key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
48+
callback: (actions, key) => {
49+
if (!key || !actions) {
50+
return;
51+
}
52+
const reportID = key.replace(ONYXKEYS.COLLECTION.REPORT_ACTIONS, '');
53+
allReportActions[reportID] = actions;
54+
},
55+
});
56+
57+
function replaceOptimisticReportWithActualReport(report: Report, draftReportComment: string | undefined) {
58+
const {reportID, preexistingReportID, parentReportID, parentReportActionID} = report;
59+
60+
if (!reportID || !preexistingReportID) {
61+
return;
62+
}
63+
64+
// Handle cleanup of stale optimistic IOU report and its report preview separately
65+
if ((isMoneyRequestReport(report) || isMoneyRequest(report)) && parentReportID && parentReportActionID) {
66+
const parentReportAction = allReportActions?.[parentReportID]?.[parentReportActionID];
67+
if (parentReportAction?.childReportID === reportID) {
68+
Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`, {
69+
[parentReportActionID]: null,
70+
});
71+
}
72+
Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, null);
73+
return;
74+
}
75+
76+
// eslint-disable-next-line @typescript-eslint/no-deprecated
77+
InteractionManager.runAfterInteractions(() => {
78+
// It is possible that we optimistically created a DM/group-DM for a set of users for which a report already exists.
79+
// In this case, the API will let us know by returning a preexistingReportID.
80+
// We should clear out the optimistically created report and re-route the user to the preexisting report.
81+
let callback = () => {
82+
const existingReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${preexistingReportID}`];
83+
84+
Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, null);
85+
Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${preexistingReportID}`, {
86+
...report,
87+
reportID: preexistingReportID,
88+
preexistingReportID: null,
89+
// Replacing the existing report's participants to avoid duplicates
90+
participants: existingReport?.participants ?? report.participants,
91+
});
92+
Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, null);
93+
};
94+
95+
if (!navigationRef.isReady()) {
96+
callback();
97+
return;
98+
}
99+
100+
// Use Navigation.getActiveRoute() instead of navigationRef.getCurrentRoute()?.path because
101+
// getCurrentRoute().path can be undefined during first navigation.
102+
// We still need getCurrentRoute() for params and screen name as getActiveRoute() only returns the path string.
103+
const activeRoute = Navigation.getActiveRoute();
104+
const currentRouteInfo = navigationRef.getCurrentRoute();
105+
const backTo = (currentRouteInfo?.params as {backTo?: Route})?.backTo;
106+
const screenName = currentRouteInfo?.name;
107+
108+
const isOptimisticReportFocused = activeRoute.includes(`/r/${reportID}`);
109+
110+
// Fix specific case: https://github.com/Expensify/App/pull/77657#issuecomment-3678696730.
111+
// When user is editing a money request report (/e/:reportID route) and has
112+
// an optimistic report in the background that should be replaced with preexisting report
113+
const isOptimisticReportInBackground = screenName === SCREENS.RIGHT_MODAL.EXPENSE_REPORT && backTo && backTo.includes(`/r/${reportID}`);
114+
115+
// Only re-route them if they are still looking at the optimistically created report
116+
if (isOptimisticReportFocused || isOptimisticReportInBackground) {
117+
const currCallback = callback;
118+
callback = () => {
119+
currCallback();
120+
if (isOptimisticReportFocused) {
121+
Navigation.setParams({reportID: preexistingReportID.toString()});
122+
} else if (isOptimisticReportInBackground) {
123+
// Navigate to the correct backTo route with the preexisting report ID
124+
Navigation.navigate(backTo.replace(`/r/${reportID}`, `/r/${preexistingReportID}`) as Route);
125+
}
126+
};
127+
128+
// The report screen will listen to this event and transfer the draft comment to the existing report
129+
// This will allow the newest draft comment to be transferred to the existing report
130+
DeviceEventEmitter.emit(`switchToPreExistingReport_${reportID}`, {
131+
preexistingReportID,
132+
callback,
133+
});
134+
135+
return;
136+
}
137+
138+
// In case the user is not on the report screen, we will transfer the report draft comment directly to the existing report
139+
// after that clear the optimistically created report
140+
if (!draftReportComment) {
141+
callback();
142+
return;
143+
}
144+
145+
saveReportDraftComment(preexistingReportID, draftReportComment, callback);
146+
});
147+
}
148+
149+
// Reports are observed only to detect preexistingReportID and run replacement; no UI subscribes, so connectWithoutView() is used.
150+
Onyx.connectWithoutView({
151+
key: ONYXKEYS.COLLECTION.REPORT,
152+
waitForCollectionCallback: true,
153+
callback: (value: OnyxCollection<Report>) => {
154+
allReports = value;
155+
156+
if (!value) {
157+
return;
158+
}
159+
160+
for (const report of Object.values(value)) {
161+
if (!report) {
162+
continue;
163+
}
164+
165+
replaceOptimisticReportWithActualReport(report, allReportDraftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${report.reportID}`]);
166+
}
167+
},
168+
});
169+
170+
export {replaceOptimisticReportWithActualReport};
171+
172+
export default {};

0 commit comments

Comments
 (0)