Skip to content

Commit dedee37

Browse files
committed
fix: prevent Concierge redirect and LHN disappearance when vacation delegate splits expense
- Add useIsOwnWorkspaceChatRef shared hook (ref persists through temporary Onyx SET wipe) - Guard isRemovalExpectedForReportType in ReportNavigateAwayHandler to exclude own workspace chats so the delegate-split Onyx wipe does not trigger a Concierge redirect (Effect 1) - Skip Effect 2 navigation entirely for own workspace chats — the re-fetch in ReportFetchHandler restores the data causing wasDeleted to reset; genuine deletions always go through Effect 1 via userLeavingStatus/didReportClose and never rely on Effect 2 alone - Add re-fetch useEffect in ReportFetchHandler to restore report after Onyx wipe (fixes skeleton) Fixes Expensify#84248
1 parent 69a3bdc commit dedee37

3 files changed

Lines changed: 70 additions & 1 deletion

File tree

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import {useRef} from 'react';
2+
import type {OnyxEntry} from 'react-native-onyx';
3+
import type * as OnyxTypes from '@src/types/onyx';
4+
5+
/**
6+
* Returns a ref that tracks whether the currently-open report is an own workspace chat.
7+
*
8+
* Must be updated synchronously during render — after a vacation delegate splits an expense
9+
* the server sends an Onyx SET that wipes the report object. By the time any useEffect fires,
10+
* `report` is already undefined, so live state and usePrevious both fail. The ref persists
11+
* the last known value through that wipe window so navigation guards and re-fetch effects
12+
* can still make the correct decision. See issue #84248.
13+
*/
14+
function useIsOwnWorkspaceChatRef(report: OnyxEntry<OnyxTypes.Report> | undefined, reportIDFromRoute: string | undefined) {
15+
const isOwnWorkspaceChatRef = useRef(false);
16+
17+
if (report?.reportID && report.reportID === reportIDFromRoute) {
18+
// Valid, matching report — update the ref.
19+
isOwnWorkspaceChatRef.current = !!report.isOwnPolicyExpenseChat;
20+
} else if (!report?.reportID) {
21+
// Report wiped by Onyx SET — intentionally keep the last known value.
22+
} else {
23+
// Different report loaded — reset.
24+
isOwnWorkspaceChatRef.current = false;
25+
}
26+
27+
return isOwnWorkspaceChatRef;
28+
}
29+
30+
export default useIsOwnWorkspaceChatRef;

src/pages/inbox/ReportFetchHandler.tsx

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {InteractionManager} from 'react-native';
44
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
55
import useIsAnonymousUser from '@hooks/useIsAnonymousUser';
66
import useIsInSidePanel from '@hooks/useIsInSidePanel';
7+
import useIsOwnWorkspaceChatRef from '@hooks/useIsOwnWorkspaceChatRef';
78
import useNetwork from '@hooks/useNetwork';
89
import useOnyx from '@hooks/useOnyx';
910
import usePaginatedReportActions from '@hooks/usePaginatedReportActions';
@@ -91,6 +92,9 @@ function ReportFetchHandler() {
9192

9293
const isTransactionThreadView = isReportTransactionThread(report);
9394

95+
// Track whether the current route is an own workspace chat. See issue #84248.
96+
const isCurrentRouteOwnWorkspaceChatRef = useIsOwnWorkspaceChatRef(report, reportIDFromRoute);
97+
9498
const indexOfLinkedMessage = reportActionIDFromRoute ? reportActions.findIndex((obj) => String(obj.reportActionID) === String(reportActionIDFromRoute)) : -1;
9599
const doesCreatedActionExists = !!reportActions?.findLast((action) => isCreatedAction(action));
96100
const isLinkedMessageAvailable = indexOfLinkedMessage > -1;
@@ -151,6 +155,21 @@ function ReportFetchHandler() {
151155

152156
// Effect order below matches the original declaration order in ReportScreen.tsx.
153157

158+
// When a delegate splits an expense the server sends a temporary Onyx SET that wipes the
159+
// workspace chat. The navigation guards in ReportScreen block any redirect, but the report
160+
// stays blank until something re-fetches it. This effect detects the wipe and re-fetches.
161+
// See issue #84248.
162+
const prevReportID = usePrevious(report?.reportID);
163+
useEffect(() => {
164+
const wasJustWiped = !!prevReportID && prevReportID === reportIDFromRoute && !report?.reportID;
165+
if (!wasJustWiped || !isCurrentRouteOwnWorkspaceChatRef.current) {
166+
return;
167+
}
168+
fetchReport();
169+
// fetchReport is a stable useEffectEvent callback and does not need to be listed as a dependency.
170+
// eslint-disable-next-line react-hooks/exhaustive-deps
171+
}, [report?.reportID, prevReportID, reportIDFromRoute]);
172+
154173
useEffect(() => {
155174
if (!transactionThreadReportID || !route?.params?.reportActionID || !isOneTransactionThread(childReport, report, linkedAction)) {
156175
return;

src/pages/inbox/ReportNavigateAwayHandler.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {useEffect, useEffectEvent, useRef} from 'react';
33
import type {OnyxEntry} from 'react-native-onyx';
44
import {useCurrentReportIDState} from '@hooks/useCurrentReportID';
55
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
6+
import useIsOwnWorkspaceChatRef from '@hooks/useIsOwnWorkspaceChatRef';
67
import useOnyx from '@hooks/useOnyx';
78
import useParentReportAction from '@hooks/useParentReportAction';
89
import usePrevious from '@hooks/usePrevious';
@@ -81,6 +82,11 @@ function ReportNavigateAwayHandler() {
8182
const isOptimisticDelete = report?.statusNum === CONST.REPORT.STATUS_NUM.CLOSED;
8283
const {wasDeleted: reportWasDeleted, parentReportID: deletedReportParentID} = useReportWasDeleted(reportIDFromRoute, report, isOptimisticDelete, userLeavingStatus);
8384

85+
// Track whether the current route is an own workspace chat. A vacation delegate split sends
86+
// a temporary Onyx SET that wipes the report; by the time effects fire, report is undefined
87+
// so we must persist the value in a ref updated synchronously during render. See issue #84248.
88+
const isCurrentRouteOwnWorkspaceChatRef = useIsOwnWorkspaceChatRef(report, reportIDFromRoute);
89+
8490
const firstRender = useRef(true);
8591

8692
// Navigation action that reads non-reactive context (concierge params, modal state, etc.)
@@ -138,7 +144,10 @@ function ReportNavigateAwayHandler() {
138144
isEmpty(report) &&
139145
(isMoneyRequest(prevReport) ||
140146
isMoneyRequestReport(prevReport) ||
141-
isPolicyExpenseChat(prevReport) ||
147+
// Own workspace chats are excluded: a vacation delegate split sends a temporary
148+
// Onyx SET that wipes the report — the chat was never intentionally removed.
149+
// See issue #84248.
150+
(isPolicyExpenseChat(prevReport) && !prevReport?.isOwnPolicyExpenseChat) ||
142151
isGroupChat(prevReport) ||
143152
isAdminRoom(prevReport) ||
144153
isAnnounceRoom(prevReport));
@@ -194,6 +203,17 @@ function ReportNavigateAwayHandler() {
194203
return;
195204
}
196205

206+
// For own workspace chats, a vacation delegate split sends a temporary Onyx SET that
207+
// silently wipes the report from Onyx — triggering this effect. We skip navigation here
208+
// entirely: the re-fetch effect in ReportFetchHandler restores the data, which causes
209+
// useReportWasDeleted to reset wasDeleted → false, and this effect exits early on the
210+
// next run. Genuine workspace deletions (e.g. user removed, workspace closed) always
211+
// trigger the "navigate on removal" effect above via userLeavingStatus / didReportClose,
212+
// never via this path alone. See issue #84248.
213+
if (isCurrentRouteOwnWorkspaceChatRef.current) {
214+
return;
215+
}
216+
197217
// Try to navigate to parent report if available
198218
if (deletedReportParentID && !isMoneyRequestReportPendingDeletion(deletedReportParentID)) {
199219
Navigation.isNavigationReady().then(() => {

0 commit comments

Comments
 (0)