Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a0da86a
refactor: Add navigateAfterExpenseCreate and dismissModalAndOpenRepor…
TaduJR Mar 29, 2026
e1cc99a
refactor: Wire up sendMoney callers to use dismissModalAndOpenReportI…
TaduJR Mar 29, 2026
03099eb
Merge branch 'main' of https://github.com/TaduJR/App into refactor-co…
TaduJR Mar 31, 2026
e66ff45
refactor: Remove shouldHandleNavigation from submitPerDiemExpense and…
TaduJR Mar 31, 2026
8c9bf99
Merge branch 'main' of https://github.com/TaduJR/App into refactor-co…
TaduJR Mar 31, 2026
d700b5e
refactor: sendMoney to params object and pre-generate optimistic chat…
TaduJR Apr 2, 2026
2b58745
fix: per-diem policy expense chat lookup and guard navigation on earl…
TaduJR Apr 2, 2026
c86cc79
fix: Add getChatByParticipants fallback for expense report chat resol…
TaduJR Apr 2, 2026
3a3eda5
fix: Extract resolveOptimisticChatReportID helper and deduplicate dis…
TaduJR Apr 2, 2026
328511c
Merge branch 'main' of https://github.com/TaduJR/App into refactor-co…
TaduJR Apr 2, 2026
5dfa313
Merge branch 'main' of https://github.com/TaduJR/App into refactor-co…
TaduJR Apr 2, 2026
dd99009
Merge branch 'main' of https://github.com/TaduJR/App into refactor-co…
TaduJR Apr 5, 2026
a275c58
Merge branch 'main' of https://github.com/TaduJR/App into refactor-co…
TaduJR Apr 6, 2026
143a765
test: Add unit tests for navigation helpers and resolveOptimisticChat…
TaduJR Apr 6, 2026
80708af
Merge branch 'main' of https://github.com/TaduJR/App into refactor-co…
TaduJR Apr 7, 2026
f4d4f40
chore: CI restart
TaduJR Apr 7, 2026
2b7d7d3
Merge branch 'main' of https://github.com/TaduJR/App into refactor-co…
TaduJR Apr 9, 2026
8a07284
fix: Restore sync-first navigation optimization and replace getReport…
TaduJR Apr 9, 2026
f83047e
Merge branch 'main' of https://github.com/TaduJR/App into refactor-co…
TaduJR Apr 13, 2026
0d3f409
Merge branch 'main' of https://github.com/TaduJR/App into refactor-co…
TaduJR Apr 13, 2026
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
13 changes: 12 additions & 1 deletion src/libs/IOUUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {getCurrencyUnit} from './CurrencyUtils';
import Navigation from './Navigation/Navigation';
import {isPaidGroupPolicy} from './PolicyUtils';
import {getOriginalMessage, isMoneyRequestAction} from './ReportActionsUtils';
import {isSelfDM} from './ReportUtils';
import {generateReportID, getChatByParticipants, isSelfDM} from './ReportUtils';
import {endSpan, getSpan, startSpan} from './telemetry/activeSpans';
import {getTagArrayFromName} from './TransactionUtils';

Expand Down Expand Up @@ -449,6 +449,16 @@ function getInitialPerDiemTargetReport(
return {targetReport, targetIouType, transactionReportID};
}

/**
* Resolves the chat report ID for navigation, generating an optimistic ID if no existing chat is found.
*/
function resolveOptimisticChatReportID(participantAccountIDs: number[], existingReport?: OnyxInputOrEntry<Report>) {
const existingChat = existingReport?.reportID ? existingReport : getChatByParticipants(participantAccountIDs);
const optimisticChatReportID = existingChat?.reportID ? undefined : generateReportID();
const chatReportID = existingChat?.reportID ?? optimisticChatReportID;
return {optimisticChatReportID, chatReportID};
}

export {
calculateAmount,
calculateSplitAmountFromPercentage,
Expand All @@ -466,4 +476,5 @@ export {
navigateToConfirmationPage,
calculateDefaultReimbursable,
getInitialPerDiemTargetReport,
resolveOptimisticChatReportID,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import {InteractionManager} from 'react-native';
import getIsNarrowLayout from '@libs/getIsNarrowLayout';
import Navigation, {navigationRef} from '@libs/Navigation/Navigation';
import {getSpan} from '@libs/telemetry/activeSpans';
import {endSubmitFollowUpActionSpan, setPendingSubmitFollowUpAction} from '@libs/telemetry/submitFollowUpAction';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import isReportOpenInRHP from './isReportOpenInRHP';
import isReportOpenInSuperWideRHP from './isReportOpenInSuperWideRHP';
import isSearchTopmostFullScreenRoute from './isSearchTopmostFullScreenRoute';
import setNavigationActionToMicrotaskQueue from './setNavigationActionToMicrotaskQueue';

/**
* After finishing the action in RHP from the Inbox tab, besides dismissing the modal, we should open the report.
* If the action is done from the report RHP, then we just want to dismiss the money request flow screens.
*/
function dismissModalAndOpenReportInInboxTab(reportID: string | undefined, isInvoice: boolean | undefined, hasMultipleTransactions: boolean) {
const rootState = navigationRef.getRootState();
const hasSubmitToDestinationVisibleSpan = !!getSpan(CONST.TELEMETRY.SPAN_SUBMIT_TO_DESTINATION_VISIBLE);

if (!isInvoice && isReportOpenInRHP(rootState)) {
const rhpKey = rootState.routes.at(-1)?.state?.key;
if (rhpKey) {
const isSuperWideRHP = isReportOpenInSuperWideRHP(rootState);

// submit_follow_up_action: only set when the span was started.
if (hasSubmitToDestinationVisibleSpan) {
if (isSuperWideRHP) {
setPendingSubmitFollowUpAction(CONST.TELEMETRY.SUBMIT_FOLLOW_UP_ACTION.DISMISS_MODAL_ONLY, reportID);
} else if (hasMultipleTransactions && reportID) {
setPendingSubmitFollowUpAction(CONST.TELEMETRY.SUBMIT_FOLLOW_UP_ACTION.DISMISS_MODAL_AND_OPEN_REPORT, reportID);
} else {
setPendingSubmitFollowUpAction(CONST.TELEMETRY.SUBMIT_FOLLOW_UP_ACTION.DISMISS_MODAL_ONLY, reportID);
}
}
// When a report is opened in the super wide RHP, we need to dismiss to the first RHP to show the same report with new expense.
if (isSuperWideRHP) {
Navigation.dismissToPreviousRHP();
return;
}
// When a report with one expense is opened in the wide RHP and the user adds another expense, RHP should be dismissed and ROUTES.SEARCH_MONEY_REQUEST_REPORT should be displayed.
if (hasMultipleTransactions && reportID) {
// On small screens, dismiss all modals and then navigate to the right report.
// On large screens, dismiss to the previous RHP first, then replace the current route with the new report.
const isNarrowLayout = getIsNarrowLayout();
if (isNarrowLayout) {
Navigation.dismissModal();
} else {
Navigation.dismissToPreviousRHP();
}
setNavigationActionToMicrotaskQueue(() => {
Navigation.navigate(ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID}), {forceReplace: !isNarrowLayout});
});
return;
}
Navigation.pop(rhpKey);
return;
}
}
if (isSearchTopmostFullScreenRoute() || !reportID) {
if (hasSubmitToDestinationVisibleSpan) {
setPendingSubmitFollowUpAction(CONST.TELEMETRY.SUBMIT_FOLLOW_UP_ACTION.DISMISS_MODAL_ONLY);
}
Navigation.dismissModal();
if (hasSubmitToDestinationVisibleSpan) {
// eslint-disable-next-line @typescript-eslint/no-deprecated -- we need to wait for the modal to be dismissed before marking the span
InteractionManager.runAfterInteractions(() => {
endSubmitFollowUpActionSpan(CONST.TELEMETRY.SUBMIT_FOLLOW_UP_ACTION.DISMISS_MODAL_ONLY);
});
}
return;
}
if (hasSubmitToDestinationVisibleSpan) {
Navigation.dismissModalWithReport({reportID}, undefined, {
onBeforeNavigate: (willOpenReport) => {
setPendingSubmitFollowUpAction(
willOpenReport ? CONST.TELEMETRY.SUBMIT_FOLLOW_UP_ACTION.DISMISS_MODAL_AND_OPEN_REPORT : CONST.TELEMETRY.SUBMIT_FOLLOW_UP_ACTION.DISMISS_MODAL_ONLY,
reportID,
);
},
});
} else {
Navigation.dismissModalWithReport({reportID});
}
}

export default dismissModalAndOpenReportInInboxTab;
81 changes: 81 additions & 0 deletions src/libs/Navigation/helpers/navigateAfterExpenseCreate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import getIsNarrowLayout from '@libs/getIsNarrowLayout';
import Log from '@libs/Log';
import Navigation, {navigationRef} from '@libs/Navigation/Navigation';
import {buildCannedSearchQuery, getCurrentSearchQueryJSON} from '@libs/SearchQueryUtils';
import {setPendingSubmitFollowUpAction} from '@libs/telemetry/submitFollowUpAction';
import CONST from '@src/CONST';
import NAVIGATORS from '@src/NAVIGATORS';
import ROUTES from '@src/ROUTES';
import SCREENS from '@src/SCREENS';
import dismissModalAndOpenReportInInboxTab from './dismissModalAndOpenReportInInboxTab';
import isReportTopmostSplitNavigator from './isReportTopmostSplitNavigator';
import isSearchTopmostFullScreenRoute from './isSearchTopmostFullScreenRoute';

type NavigateAfterExpenseCreateParams = {
activeReportID?: string;
transactionID?: string;
isFromGlobalCreate?: boolean;
isInvoice?: boolean;
hasMultipleTransactions: boolean;
};

/**
* Helper to navigate after an expense is created in order to standardize the post‑creation experience
* when creating an expense from the global create button.
* If the expense is created from the global create button then:
* - If it is created on the inbox tab, it will open the chat report containing that expense.
* - If it is created elsewhere, it will navigate to Reports > Expense and highlight the newly created expense.
*/
function navigateAfterExpenseCreate({activeReportID, transactionID, isFromGlobalCreate, isInvoice, hasMultipleTransactions}: NavigateAfterExpenseCreateParams) {
Comment thread
TaduJR marked this conversation as resolved.
const isUserOnInbox = isReportTopmostSplitNavigator();

// If the expense is not created from global create or is currently on the inbox tab,
// we just need to dismiss the money request flow screens
// and open the report chat containing the IOU report
if (!isFromGlobalCreate || isUserOnInbox || !transactionID) {
dismissModalAndOpenReportInInboxTab(activeReportID, isInvoice, hasMultipleTransactions);
return;
}

const type = isInvoice ? CONST.SEARCH.DATA_TYPES.INVOICE : CONST.SEARCH.DATA_TYPES.EXPENSE;

// When already on Search ROOT with the same type (expense vs invoice), we navigate to the same screen (no-op or refresh); record as dismiss_modal_only.
// When on another Search sub-tab (e.g. Chats), or on Search with a different type (e.g. on Invoice, submitting expense), record as navigate_to_search.
const rootState = navigationRef.getRootState();
const searchNavigatorRoute = rootState?.routes?.findLast((route) => route.name === NAVIGATORS.SEARCH_FULLSCREEN_NAVIGATOR);
const lastSearchRoute = searchNavigatorRoute?.state?.routes?.at(-1);
const alreadyOnSearchRoot = isSearchTopmostFullScreenRoute() && lastSearchRoute?.name === SCREENS.SEARCH.ROOT;
const currentSearchQueryJSON = alreadyOnSearchRoot ? getCurrentSearchQueryJSON() : undefined;
const isSameSearchType = currentSearchQueryJSON?.type === type;
setPendingSubmitFollowUpAction(
alreadyOnSearchRoot && isSameSearchType ? CONST.TELEMETRY.SUBMIT_FOLLOW_UP_ACTION.DISMISS_MODAL_ONLY : CONST.TELEMETRY.SUBMIT_FOLLOW_UP_ACTION.NAVIGATE_TO_SEARCH,
);

const queryString = buildCannedSearchQuery({type});
const navigateToSearch = () => {
// On the fast path, onConfirm already cleared the flag and dismissed the modal,
// so this branch is only reached on the slow path (user submitted before the
// 300ms pre-insert timer fired).
if (getIsNarrowLayout() && Navigation.getIsFullscreenPreInsertedUnderRHP()) {
Navigation.clearFullscreenPreInsertedFlag();
Navigation.dismissModal();
} else if (getIsNarrowLayout()) {
const isRHPStillOnTop = navigationRef.getRootState()?.routes?.at(-1)?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR;
if (!alreadyOnSearchRoot || !isSameSearchType || isRHPStillOnTop) {
Navigation.navigate(ROUTES.SEARCH_ROOT.getRoute({query: queryString}), {forceReplace: true});
} else {
Log.info('[IOU] navigateToSearch: already on matching Search root with RHP dismissed — no-op');
}
} else {
Navigation.revealRouteBeforeDismissingModal(ROUTES.SEARCH_ROOT.getRoute({query: queryString}));
}
};

if (navigationRef.isReady()) {
navigateToSearch();
} else {
Navigation.isNavigationReady().then(navigateToSearch);
}
}

export default navigateAfterExpenseCreate;
16 changes: 8 additions & 8 deletions src/libs/actions/IOU/PerDiem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ import {
getPolicyTags,
getReportPreviewAction,
getUserAccountID,
handleNavigateAfterExpenseCreate,
highlightTransactionOnSearchRouteIfNeeded,
mergePolicyRecentlyUsedCategories,
mergePolicyRecentlyUsedCurrencies,
Expand Down Expand Up @@ -232,10 +231,10 @@ type PerDiemExpenseInformation = {
betas: OnyxEntry<OnyxTypes.Beta[]>;
customUnitPolicyID?: string;
personalDetails: OnyxEntry<OnyxTypes.PersonalDetailsList>;
shouldHandleNavigation?: boolean;
shouldPlaySound?: boolean;
optimisticReportPreviewActionID?: string;
shouldDeferAutoSubmit?: boolean;
optimisticChatReportID?: string;
};

type PerDiemExpenseInformationParams = {
Expand All @@ -255,6 +254,7 @@ type PerDiemExpenseInformationParams = {
betas: OnyxEntry<OnyxTypes.Beta[]>;
optimisticReportPreviewActionID?: string;
personalDetails: OnyxEntry<OnyxTypes.PersonalDetailsList>;
optimisticChatReportID?: string;
};

type PerDiemExpenseInformationForSelfDM = {
Expand Down Expand Up @@ -303,6 +303,7 @@ function getPerDiemExpenseInformation(perDiemExpenseInformation: PerDiemExpenseI
betas,
optimisticReportPreviewActionID,
personalDetails,
optimisticChatReportID,
} = perDiemExpenseInformation;
const {payeeAccountID = getUserAccountID(), payeeEmail = getCurrentUserEmail(), participant} = participantParams;
const {policy, policyCategories, policyTagList, policyRecentlyUsedCategories, policyRecentlyUsedTags} = policyParams;
Expand Down Expand Up @@ -340,6 +341,7 @@ function getPerDiemExpenseInformation(perDiemExpenseInformation: PerDiemExpenseI
isNewChatReport = true;
chatReport = buildOptimisticChatReport({
participantList: [payerAccountID, payeeAccountID],
optimisticReportID: optimisticChatReportID,
currentUserAccountID: currentUserAccountIDParam,
});
}
Expand Down Expand Up @@ -890,12 +892,11 @@ function submitPerDiemExpense(submitPerDiemExpenseInformation: PerDiemExpenseInf
betas,
customUnitPolicyID,
personalDetails,
shouldHandleNavigation = true,
shouldPlaySound: shouldPlaySoundParam = true,
optimisticReportPreviewActionID,
shouldDeferAutoSubmit,
optimisticChatReportID,
} = submitPerDiemExpenseInformation;
const {payeeAccountID} = participantParams;
const {currency, comment = '', category, tag, created, customUnit, attendees, isFromGlobalCreate} = transactionParams;

if (
Expand Down Expand Up @@ -944,6 +945,7 @@ function submitPerDiemExpense(submitPerDiemExpenseInformation: PerDiemExpenseInf
betas,
optimisticReportPreviewActionID,
personalDetails,
optimisticChatReportID,
});

const activeReportID = isMoneyRequestReport && Navigation.getTopmostReportId() === report?.reportID ? report?.reportID : chatReport.reportID;
Expand Down Expand Up @@ -988,7 +990,7 @@ function submitPerDiemExpense(submitPerDiemExpenseInformation: PerDiemExpenseInf
playSound(SOUNDS.DONE);
}

const shouldDeferWrite = shouldHandleNavigation && isFromGlobalCreate && !isReportTopmostSplitNavigator();
const shouldDeferWrite = isFromGlobalCreate && !isReportTopmostSplitNavigator();
const apiWrite = () => {
API.write(WRITE_COMMANDS.CREATE_PER_DIEM_REQUEST, parameters, onyxData);
};
Expand All @@ -1006,10 +1008,8 @@ function submitPerDiemExpense(submitPerDiemExpenseInformation: PerDiemExpenseInf

highlightTransactionOnSearchRouteIfNeeded(isFromGlobalCreate, transaction.transactionID, CONST.SEARCH.DATA_TYPES.EXPENSE);

handleNavigateAfterExpenseCreate({activeReportID, transactionID: transaction.transactionID, isFromGlobalCreate, shouldHandleNavigation});

if (activeReportID) {
notifyNewAction(activeReportID, undefined, payeeAccountID === currentUserAccountIDParam);
notifyNewAction(activeReportID, undefined, participantParams.payeeAccountID === currentUserAccountIDParam);
}

return {iouReport};
Expand Down
Loading
Loading