Skip to content

Commit 35fa511

Browse files
Fix: dismiss RHP on narrow layout for Looking-Around self-DM skip-confirmation submits
Co-authored-by: Shawn Borton <shawnborton@users.noreply.github.com>
1 parent 7755cd1 commit 35fa511

2 files changed

Lines changed: 48 additions & 2 deletions

File tree

src/libs/Navigation/helpers/submitWithDismissFirst.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,19 @@ function submitWithDismissFirst({
9797
if (isFromGlobalCreate && isLookingAroundUser && isSelfDMDestination) {
9898
startTracking(telemetryContext, {skipSubmitExpenseSpan: true});
9999
setFastPath(CONST.TELEMETRY.FAST_PATH_HANDLER.DEFAULT);
100+
// On narrow layout there is no pre-inserted route to reveal (getSkipConfirmationPreMountDestinationRoute suppresses
101+
// the self-DM pre-insert for these users), and navigateAfterExpenseCreate's narrow branch only switches the tab
102+
// beneath the RHP via Navigation.navigate - it never closes the Create Expense modal, so the user is stranded on an
103+
// empty "Create Expense" page with the receipt hidden. Dismiss the modal first (mirroring the confirmation flow's
104+
// handleSearchDismiss), then hand navigation to the write, which routes to Search once the modal has closed. Wide
105+
// layout keeps the direct write: there navigateAfterExpenseCreate reveals Search via revealRouteBeforeDismissingModal,
106+
// which dismisses the modal itself, so an explicit dismiss here would be redundant (and revealing after it would break).
107+
if (getIsNarrowLayout()) {
108+
Navigation.dismissModal({
109+
afterTransition: () => executeWrite({shouldHandleNavigation: true}),
110+
});
111+
return;
112+
}
100113
executeWrite({shouldHandleNavigation: true});
101114
return;
102115
}

tests/unit/submitWithDismissFirstTest.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ const mockGetReportOrDraftReport = jest.fn();
1111
const mockDismissModal = jest.fn<ReturnType<DismissModal>, Parameters<DismissModal>>();
1212
const mockRevealRouteBeforeDismissingModal = jest.fn<ReturnType<RevealRouteBeforeDismissingModal>, Parameters<RevealRouteBeforeDismissingModal>>();
1313
const mockGetIsFullscreenPreInsertedUnderRHP = jest.fn<boolean, []>();
14+
const mockGetIsNarrowLayout = jest.fn<boolean, []>();
1415
const mockReserveDeferredWriteChannel = jest.fn();
1516
const mockStartTracking = jest.fn();
1617
const mockSetFastPath = jest.fn();
1718
const mockSetPendingSubmitFollowUpAction = jest.fn();
1819

1920
jest.mock('@libs/Navigation/helpers/isSearchTopmostFullScreenRoute', () => () => mockIsSearchTopmostFullScreenRoute());
21+
jest.mock('@libs/getIsNarrowLayout', () => () => mockGetIsNarrowLayout());
2022
jest.mock('@libs/Navigation/Navigation', () => ({
2123
dismissModal: mockDismissModal,
2224
revealRouteBeforeDismissingModal: mockRevealRouteBeforeDismissingModal,
@@ -52,6 +54,7 @@ describe('submitWithDismissFirst', () => {
5254
mockIsSearchTopmostFullScreenRoute.mockReturnValue(false);
5355
mockGetReportOrDraftReport.mockReturnValue(undefined);
5456
mockGetIsFullscreenPreInsertedUnderRHP.mockReturnValue(false);
57+
mockGetIsNarrowLayout.mockReturnValue(false);
5558
});
5659

5760
describe('Search-topmost branch', () => {
@@ -201,10 +204,12 @@ describe('submitWithDismissFirst', () => {
201204
isSelfDMDestination: true,
202205
};
203206

204-
it('hands navigation to the write instead of revealing the self-DM report', () => {
207+
it('on wide layout hands navigation to the write instead of revealing the self-DM report', () => {
205208
// Without this branch the destination-report fast path reveals the self-DM and calls executeWrite with
206209
// shouldHandleNavigation: false, which makes cleanupAfterSkipConfirmSubmit drop the routing flags before
207-
// navigateAfterExpenseCreate can route these users to Spend > Expenses.
210+
// navigateAfterExpenseCreate can route these users to Spend > Expenses. On wide layout navigateAfterExpenseCreate
211+
// reveals Search (which dismisses the modal itself), so no explicit dismiss happens here.
212+
mockGetIsNarrowLayout.mockReturnValue(false);
208213
mockGetReportOrDraftReport.mockReturnValue({reportID: 'selfDM1'});
209214
const executeWrite = jest.fn();
210215

@@ -220,6 +225,34 @@ describe('submitWithDismissFirst', () => {
220225
expect(mockDismissModal).not.toHaveBeenCalled();
221226
});
222227

228+
it('on narrow layout dismisses the modal first, then hands navigation to the write in afterTransition', () => {
229+
// Regression guard for https://github.com/Expensify/App/pull/97883: on narrow layout navigateAfterExpenseCreate
230+
// only switches the tab beneath the RHP, so the Create Expense modal must be dismissed here or the user is
231+
// stranded on an empty "Create Expense" page after a skip-confirmation (e.g. QAB scan) submit.
232+
mockGetIsNarrowLayout.mockReturnValue(true);
233+
mockGetReportOrDraftReport.mockReturnValue({reportID: 'selfDM1'});
234+
const executeWrite = jest.fn();
235+
236+
submitWithDismissFirst({
237+
executeWrite,
238+
destinationReportID: 'selfDM1',
239+
telemetryContext: TELEMETRY_CONTEXT,
240+
...LOOKING_AROUND_SELF_DM,
241+
});
242+
243+
expect(mockDismissModal).toHaveBeenCalledTimes(1);
244+
expect(mockRevealRouteBeforeDismissingModal).not.toHaveBeenCalled();
245+
// The write is deferred until the modal transition completes.
246+
expect(executeWrite).not.toHaveBeenCalled();
247+
248+
const [dismissOptions] = mockDismissModal.mock.calls.at(0) ?? [];
249+
if (!dismissOptions?.afterTransition) {
250+
throw new Error('Expected dismissModal afterTransition callback');
251+
}
252+
dismissOptions.afterTransition();
253+
expect(executeWrite).toHaveBeenCalledWith({shouldHandleNavigation: true});
254+
});
255+
223256
it('still starts tracking so telemetry is not skipped', () => {
224257
mockGetReportOrDraftReport.mockReturnValue({reportID: 'selfDM1'});
225258

0 commit comments

Comments
 (0)