You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Due for payment 2026-07-29] [$250] Chat - The new group chat doesn't open in Firefox #94571
Version Number: 9.4.19-1 Reproducible in staging?: Yes Reproducible in production?: Yes If this was caught during regression testing, add the test name, ID and link from BrowserStack:https://github.com/Expensify/Expensify/issues/537811 Email or phone of affected tester (no customers): - Issue reported by: Applause Internal Team Bug source: Regression TC Execution Device used: macOS Sequoia 15.4.1 - Firefox
⚠️ Medium confidence: the code path is firmly identified and frontend-only, but the precise reason Firefox alone fails needs a Firefox repro with logging to nail down (see open question at the end).
Please re-state the problem that we are trying to solve in this issue.
In Firefox, creating a group chat (Start Chat → Add to group → Next → Start Group) does not open the new chat — the user is left in the previously-open report. It works in Chrome/Safari.
What is the root cause of that problem?
This is a client-side navigation timing race, not a backend issue. The group report is created optimistically and navigation never waits on the server, so the failure is purely in the dismiss-modal-then-navigate sequence.
On a separate setTimeout(0) → Navigation.navigate(REPORT_WITH_ID) to open the new report.
The code comment on that setTimeout ("In some cases when RHP modal gets hidden and then we navigate to report Composer focus breaks...") already documents this sequence as timing-fragile.
Both the modal dismissal and the navigation are gated through TransitionTracker, whose "modal close finished" signal on web is fired by a react-native-reanimated Keyframe exit-animation callback: new Keyframe(...).withCallback(() => onCloseCallbackRef.current()). That same file already warns these web callbacks are unreliable across engines ("on web the callbacks are not called when animations are disabled with the reduced motion setting on"). Reanimated/Web Animations completion callbacks are historically inconsistent in Firefox, so the dismiss-vs-navigate ordering resolves differently there: the modal-close work lands after the queued navigate and effectively supersedes it, leaving the user on the previous chat.
There is no Firefox-specific guard anywhere in this navigation/modal path today (getBrowser() exists but isn't used here).
TransitionTracker flushes queued callbacks only when activeTransitions reaches 0, with a 1000ms (MAX_TRANSITION_DURATION_MS) safety-net timeout — so a missed/late close callback strands or re-orders the queued navigate.
No Firefox-specific branch exists; Firefox follows the generic web path, so the divergence comes from browser-engine animation-callback timing.
What changes do you think we should make in order to solve the problem?
Make the navigate-to-new-report independent of the modal-close animation callback firing, so Firefox can't drop it:
In navigateToReport, use the navigation API that performs dismiss + navigate atomically (e.g. Navigation.dismissModalWithReport / passing the target report to the dismiss) instead of dismissModal() followed by a decoupled setTimeout(0) navigate. This removes the ordering race between the two dispatches.
And/or guarantee the modal-close transition always completes on web: have Container/index.web.tsx back the withCallback with a setTimeout(animationOutTiming) fallback so onCloseCallBack/endTransition fires even when the Reanimated/WAAPI completion callback doesn't fire in Firefox.
What alternative solutions did you explore?
A Firefox-only branch via getBrowser() to skip the deferred-navigate timing — works but adds a browser special-case the codebase otherwise avoids; the atomic dismiss-and-navigate fix is preferable as it helps all engines.
Open question to confirm before implementing
We should reproduce in Firefox with logging around Container/index.web.tsx:56 and the DISMISS_MODAL dispatch to confirm whether (a) the exit-animation callback fails to fire, or (b) the dismiss dispatch lands after the queued navigate. Both point at the same fix area, but the repro tells us which of the two changes above is sufficient.
Next Steps for Contributor+ team: To accept:@MelvinBot implement [this](https://github.com/Expensify/App/issues/94571) to create a draft PR. To refine:@MelvinBot <your feedback> To reject: Explain why you are rejecting Melvin's proposal.
Inside navigateToReport(), the default path does two separate things:
Calls Navigation.dismissModal() to close the new chat RHP.
Schedules Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID)) in a setTimeout(0).
Those two operations are not actually sequenced. The code assumes the modal dismissal will be far enough along by the time the timer fires, but Firefox does not consistently resolve that ordering the same way. As a result, the group report is created and can appear in the LHN, but the visible report stays on the previously selected chat.
What changes do you think we should make in order to solve the problem?
Update navigateToReport() so the “dismiss modal, then open report” case uses the existing modal-aware helper instead of doing those as two separate navigation operations.
When shouldDismissModal is true and reportID is defined, call:
Keep the current dismiss-only behavior when there is no reportID, and keep the direct Navigation.navigate() behavior for callers that pass shouldDismissModal: false.
This is better than manually nesting Navigation.navigate() inside dismissModal({afterTransition}) because dismissModalWithReport() already handles the report-specific edge cases, including when the target report is already active and when the topmost route is a super-wide RHP report.
It also preserves the existing afterTransition callback, so clearGroupChat still runs after the report navigation flow rather than immediately after the optimistic report is created.
The existing helper already implements the sequencing we need here:
instead of dismissing the modal and navigating to the report independently.
We should also manually regression-test the Firefox web flow:
Start Chat -> Add to group -> Next -> Start Group
and confirm the newly created group opens.
Fixed Demo:
REC-20260625174635.mp4
What alternative solutions did you explore? (Optional)
I considered changing only navigateToAndCreateGroupChat() to call dismissModalWithReport() directly, but the fragile sequencing is in the shared navigateToReport() helper. Fixing the helper also covers nearby create/open-report flows that dismiss a modal before opening a report.
I also considered keeping the current structure and only delaying the Navigation.navigate() call longer, but that would still be timing-based. The code already has a helper for this exact dismiss-then-open-report flow, so we should use that instead of relying on a timer.
Please re-state the problem that we are trying to solve in this issue.
In Firefox on macOS, creating a new group chat (+ → Start Chat → Add to group → Next → Start Group) does not open the newly created group. The modal closes but the user is left on whatever chat they were previously viewing instead of being taken to the new group room. This reproduces in staging and production and is specific to Firefox.
What is the root cause of that problem?
The group-creation confirm handler createGroup in NewChatConfirmPage calls navigateToAndCreateGroupChat, which builds the optimistic group report and then routes the user with navigateToReport(newChat.reportID, {afterTransition: clearGroupChat}):
Inside navigateToReport, shouldDismissModal defaults to true, so it fires Navigation.dismissModal() (which dispatches DISMISS_MODAL → StackActions.pop() to close the RHP and starts the modal-close transition), and then schedules the navigation to the new report in a bare setTimeout(0):
The two navigation dispatches are not chained — they race. dismissModal() pops the modal and triggers a transitionStart/transitionEnd cycle tracked by TransitionTracker (a pop animation drives ScreenLayout's transitionStart listener, incrementing activeTransitions). Meanwhile the setTimeout(0) callback later calls Navigation.navigate(route), and navigate runs its linkTo with runImmediately = true (no waitForTransition), so the report-push is dispatched independently of when the modal-close pop actually settles. On Chrome the modal-close pop animation and the setTimeout(0) navigate happen to interleave such that the report push lands on the resulting state. Firefox sequences the requestAnimationFrame-driven transition events and the setTimeout(0) macrotask differently, so the navigate fires against a navigation state that is still mid-pop; the report push is superseded/dropped by the in-flight DISMISS_MODAL pop, and the user is left on the previous chat. This is the classic dismiss-then-navigate race — note the canonical helpers in the codebase never use a free-floating setTimeout; they chain the navigate to the dismiss completion (submitWithDismissFirst and dismissModalWithReport both pass the navigate as the dismiss's afterTransition).
What changes do you think we should make in order to solve the problem?
Stop racing the navigate against the modal dismiss inside navigateToReport, and instead chain it to the dismiss completion exactly like the other dismiss-then-navigate flows. When shouldDismissModal is true, pass the report navigation as dismissModal's afterTransition callback so the report push only runs once the DISMISS_MODAL pop transition has settled, with clearGroupChat chained after that. The existing TransitionTracker.runAfterTransitions({waitForUpcomingTransition: true}) path already guarantees this fires after the pop (and has a MAX_TRANSITION_START_WAIT_MS safety net), so no new timing primitives are needed. The core change inside navigateToReport:
This keeps the non-dismiss callers (the shouldDismissModal: false paths at L2414/L2423/L2449) on the existing immediate-navigate behavior, and the existing DM/group-creation callers continue to dismiss first — only now the report push is deterministically ordered after the modal close instead of guessed via setTimeout(0). Because the navigate is a real React Navigation dispatch (not a focus workaround), this also removes the fragile Composer-focus setTimeout comment justification while preserving correct ordering across Firefox, Chrome, and native.
What alternative solutions did you explore? (Optional)
Bumping the setTimeout(0) delay or wrapping the navigate in requestAnimationFrame would paper over the Firefox timing without removing the race, and would slow the transition on every browser; chaining off the dismiss transition is both deterministic and consistent with the established submitWithDismissFirst / dismissModalWithReport pattern.
The group chat is created, but the app does not reliably navigate to it on Firefox because the group creation flow dismisses the modal and opens the new report as two separate navigation operations.
NewChatConfirmPage calls navigateToAndCreateGroupChat() when the user presses Start Group:
The problem is inside navigateToReport(): it first calls Navigation.dismissModal() and then schedules Navigation.navigate(route) separately with setTimeout(0):
On Firefox, the modal dismissal/browser history update can complete while the separate scheduled report navigation does not become the final active route. The recording confirms this: the new group appears in recents, but the URL and main pane stay on the previous Concierge report.
What changes do you think we should make in order to solve the problem?
Use the existing coordinated modal-dismiss-and-report-open helper when navigateToReport() needs to dismiss a modal.
If reportID is missing, keep the existing dismiss-only behavior.
If shouldDismissModal is true and reportID exists, call Navigation.dismissModalWithReport({reportID}, undefined, {afterTransition}).
Return immediately after that coordinated path.
Keep the current asynchronous direct navigation path only for shouldDismissModal: false.
Tests that should be added:
Add or update a navigation/action test around group chat creation to verify that starting a group chat dismisses the modal and opens the new report through Navigation.dismissModalWithReport.
Manually verify on Firefox web: start from an existing chat, create a group chat via Start Chat > Add to group > Next > Start Group, and confirm the new group report opens.
I considered changing NewChatConfirmPage to navigate differently only for group chat creation, but that would keep the race-prone helper available for the same modal-to-report flow elsewhere. Fixing navigateToReport() at the shared action/navigation boundary is smaller and handles every caller that wants to dismiss a modal and open a report.
What alternative solutions did you explore? (Optional)
🚨 Edited by proposal-police: This proposal was edited at 2026-06-25 12:16:45 UTC.
Proposal
Please re-state the problem that we are trying to solve in this issue.
After creating a new group chat (+ > Start Chat > Add to group > Next > Start Group), the new group chat does not open in Firefox — the user stays in the previously open chat.
What is the root cause of that problem?
Group creation ends in navigateToReport, which dismisses the modal and navigates to the new report as two uncoordinated operations:
Navigation.dismissModal();// no afterTransitionsetTimeout(()=>{Navigation.isNavigationReady().then(()=>{Navigation.navigate(route,{afterTransition: options.afterTransition});});},0);
dismissModal() is called without its afterTransition hook, and the navigate() is fired from a bare setTimeout(…, 0) with no waitForTransition. Inside navigate, runImmediately = !options?.waitForTransition is therefore true, so the navigation runs immediately rather than queuing behind the in-flight modal-dismiss transition:
The fixed 0ms macrotask delay is a best-effort guess at "the modal has finished dismissing". It happens to be long enough on Chromium, but Firefox schedules the modal-dismiss transition differently, so navigate() runs while the RHP dismissal is still in flight and the navigation to the new report gets dropped — leaving the user on the previous chat. This is why it reproduces only in Firefox.
What changes do you think we should make in order to solve the problem?
Stop racing a setTimeout, and instead navigate after the dismiss transition actually completes by passing the navigation as the afterTransition of dismissModal. dismissModal already routes afterTransition through runAfterTransitions({..., waitForUpcomingTransition: true}), which waits for the dismiss transition to start and end (with a safety fallback), so ordering is guaranteed across browsers:
This also unifies the reportID/no-reportID branches, which today already pass afterTransition to dismissModal for the no-reportID case but not for the reportID case.
Alternatively, the minimal one-line fix is to keep the existing structure but make the navigation queue behind the active dismiss transition instead of running immediately, by passing waitForTransition: true:
Note: the setTimeout(…, 0) was originally added so the RHP modal is hidden before navigating (to avoid Composer focus breaking). Running the navigation as the dismiss afterTransition happens strictly after the transition completes, so that concern is still satisfied — and more reliably than the fixed 0ms delay.
What specific scenarios should we cover in automated tests to prevent reintroducing this issue in the future?
Add a test around navigateToReport/navigateToAndCreateGroupChat asserting that Navigation.navigate to the new report's route is invoked after dismissModal resolves its transition (i.e. ordering does not depend on a setTimeout(0) race), so the new report is the active route after group creation.
What alternative solutions did you explore? (Optional)
The waitForTransition: true variant above is the smallest possible change. A heavier alternative would be replacing setTimeout(0) with requestAnimationFrame/InteractionManager, but that keeps the same "guess the timing" fragility that caused the Firefox divergence, rather than explicitly coordinating with the dismiss transition.
🚨 Edited by proposal-police: This proposal was edited at 2026-06-25 12:22:24 UTC.
Proposal
Problem
When a Firefox user creates a new group chat (+ LHN → Start Chat → Add to group → Next → Start Group), the app fails to navigate to the newly created group chat and instead leaves the user on the previous chat room. This is a regression on Firefox web only; Chrome and Safari are unaffected.
Root Cause
The bug is in navigateToReport in src/libs/actions/Report/index.ts (lines 2240–2265).
When called with a reportID (the group-chat case), the function follows this path:
// line 2248 — BROKEN: dismissModal fires without afterTransitionNavigation.dismissModal();// line 2254 — navigate fires WHILE dismiss animation is still runningsetTimeout(()=>{Navigation.isNavigationReady().then(()=>{Navigation.navigate(route,{afterTransition: clearGroupChat});// line 2259});},0);
This fires navigate(route) while the modal dismiss animation is still in progress. In Chrome and Safari, two concurrent navigation actions are processed without conflict. In Firefox, the animation timing causes the report navigation to be dropped, leaving the user on the previous chat.
There is an asymmetry inside navigateToReport that makes the bug visible:
!reportID branch (line 2244): correctly passes afterTransition to dismissModal, so the full callback is properly deferred until after the dismiss animation finishes.
reportID branch (line 2248): calls dismissModal() with no afterTransition, then fires navigate() immediately in setTimeout(0) — racing the dismiss animation.
Secondary effect:runAfterTransitions({callback: clearGroupChat, waitForUpcomingTransition: true}) registered inside navigate waits for the nexttransitionStart event (via promiseForNextTransitionStart). In Firefox, the dismiss animation's transitionStart may fire after navigate() is called, consuming the promise. This causes clearGroupChat to run after the dismiss transition rather than the report navigation transition — wrong timing, though not the primary cause of the navigation failure.
Working Analog
dismissModalWithReport in src/libs/Navigation/Navigation.ts (lines 852–889) already solves this correctly:
This chains: dismiss → wait for dismiss transitionEnd → then navigate → wait for report navigation transitionEnd → then run afterTransition. dismissModal's internal chaining (Navigation.ts:833–835) already handles this correctly:
No change to dismissModal is needed — it already chains afterTransition correctly.
Fix
src/libs/actions/Report/index.ts — navigateToReport function (lines 2240–2265)
When shouldDismissModal && reportID, mirror the dismissModalWithReport pattern: nest the navigate call insidedismissModal's afterTransition instead of firing it concurrently:
functionnavigateToReport(reportID: string|undefined,options?: {shouldDismissModal?: boolean;afterTransition?: ()=>void}){constshouldDismissModal=options?.shouldDismissModal??true;if(shouldDismissModal){if(!reportID){Navigation.dismissModal({afterTransition: options?.afterTransition});return;}// Chain navigate inside dismissModal's afterTransition so it only fires after// the dismiss animation completes — matching the dismissModalWithReport pattern.Navigation.dismissModal({afterTransition: ()=>{// setTimeout preserves the existing fix for Composer focus breaking// when RHP modal hides and navigation occurs (see original comment).setTimeout(()=>{Navigation.isNavigationReady().then(()=>{constroute=ROUTES.REPORT_WITH_ID.getRoute(reportID);Navigation.navigate(route,{afterTransition: options?.afterTransition});});},0);},});return;}if(!reportID){return;}setTimeout(()=>{Navigation.isNavigationReady().then(()=>{constroute=ROUTES.REPORT_WITH_ID.getRoute(reportID);if(options?.afterTransition){Navigation.navigate(route,{afterTransition: options.afterTransition});}else{Navigation.navigate(route);}});},0);}
shouldDismissModal: false callers skip the modal-dismiss path entirely and are unaffected.
Alternatives Considered
Increase MAX_TRANSITION_START_WAIT_MS — symptom workaround; slows all platforms and does not fix the root race condition.
Move clearGroupChat to dismissModal's afterTransition directly — incorrect; this would clear the group chat draft after dismiss but never navigate to the new report.
Remove setTimeout(0) wrapper inside the chained call — the setTimeout(0) comment in the original code notes it fixes Composer focus breaking when navigating immediately after an RHP modal closes; it should be preserved inside the chained afterTransition.
functionnavigateToReport(reportID: string|undefined,shouldDismissModal=true){if(shouldDismissModal){Navigation.dismissModal();}if(!reportID){return;}// In some cases when RHP modal gets hidden and then we navigate to report Composer focus breaks, wrapping navigation in setTimeout fixes thissetTimeout(()=>{Navigation.isNavigationReady().then(()=>Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID)));},0);}
Navigation.dismissModal() closes the New Chat RHP, which itself drives a navigation back to the previously open report. Separately, a setTimeout(0) schedules Navigation.navigate(<new group chat>). These two aren't chained — the navigate is a bare macrotask that doesn't wait for the dismiss transition to finish.
On Chromium/WebKit the ordering happens to land the navigate last, so the new chat opens. Firefox sequences the history updates and the dismiss transition differently, so the modal-dismiss's own navigation (back to the previous report) resolves after the setTimeout(0) navigate — clobbering it. The result is exactly the reported symptom: the user is left in the previous chat room instead of the new group.
navigateToReport predates / doesn't use that helper and instead leans on the fragile setTimeout(0), which is what breaks under Firefox's timing.
What changes do you think we should make in order to solve the problem?
Chain the navigation to the dismiss completion instead of racing it on a setTimeout. The minimal change is to use dismissModal's afterTransition callback in navigateToReport:
function navigateToReport(reportID: string | undefined, shouldDismissModal = true) {
- if (shouldDismissModal) {- Navigation.dismissModal();- }
if (!reportID) {
return;
}
- // In some cases when RHP modal gets hidden and then we navigate to report Composer focus breaks, wrapping navigation in setTimeout fixes this- setTimeout(() => {- Navigation.isNavigationReady().then(() => Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID)));- }, 0);+ const reportRoute = ROUTES.REPORT_WITH_ID.getRoute(reportID);+ if (shouldDismissModal) {+ // Run the navigation after the modal-dismiss transition completes instead of racing it on a+ // setTimeout(0). Firefox resolves that race in favor of the dismiss's own navigation, leaving+ // the user on the previous report. afterTransition also fires once the modal is fully hidden,+ // which preserves the composer-focus fix the setTimeout was originally added for.+ Navigation.dismissModal({afterTransition: () => Navigation.navigate(reportRoute)});+ return;+ }+ Navigation.isNavigationReady().then(() => Navigation.navigate(reportRoute));
}
afterTransition runs via TransitionTracker.runAfterTransitionsafter the dismiss completes — strictly more correct than setTimeout(0), and it preserves the composer-focus behavior the old comment was guarding (the navigate still happens after the RHP is hidden). The shouldDismissModal = false callers keep navigating directly with no behavior change.
if (shouldDismissModal) {
- Navigation.dismissModal();+ Navigation.dismissModalWithReport({reportID});+ return;
}
Either way the fix is the same idea — stop racing the navigate against the dismiss. I'd lean toward delegating to dismissModalWithReport so there's one canonical dismiss-then-open-report path and the setTimeout hack is removed entirely. All five shouldDismissModal = true call sites (group chat creation + the DM open paths) benefit, so this also fixes the same Firefox race if it shows up when opening a fresh DM.
What alternative solutions did you explore?
Increase the setTimeout delay (e.g. setTimeout(..., 100)). Rejected — it's guessing at Firefox's transition timing; it'd be flaky across machines and still racy. Chaining on the actual transition end is deterministic.
Navigate first, then dismiss the modal. Risky ordering in the other direction and can leave the RHP visible mid-navigation; dismissModalWithReport's established order (dismiss → afterTransition navigate) is the proven sequence.
Special-case Firefox. A browser sniff to branch navigation logic is brittle and leaves the underlying race in place for any other engine with similar timing. Fixing the chaining addresses the root cause for all browsers.
Activity
MelvinBot commentedon Jun 25, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
In Firefox, creating a group chat (Start Chat → Add to group → Next → Start Group) does not open the new chat — the user is left in the previously-open report. It works in Chrome/Safari.
What is the root cause of that problem?
This is a client-side navigation timing race, not a backend issue. The group report is created optimistically and navigation never waits on the server, so the failure is purely in the dismiss-modal-then-navigate sequence.
"Start Group" →
createGroup→navigateToAndCreateGroupChat→navigateToReport(newReportID, {afterTransition: clearGroupChat}).navigateToReportdoes two things in sequence:Navigation.dismissModal()— closes the New Chat RHP modal.setTimeout(0)→Navigation.navigate(REPORT_WITH_ID)to open the new report.The code comment on that
setTimeout("In some cases when RHP modal gets hidden and then we navigate to report Composer focus breaks...") already documents this sequence as timing-fragile.Both the modal dismissal and the navigation are gated through
TransitionTracker, whose "modal close finished" signal on web is fired by a react-native-reanimated Keyframe exit-animation callback:new Keyframe(...).withCallback(() => onCloseCallbackRef.current()). That same file already warns these web callbacks are unreliable across engines ("on web the callbacks are not called when animations are disabled with the reduced motion setting on"). Reanimated/Web Animations completion callbacks are historically inconsistent in Firefox, so the dismiss-vs-navigate ordering resolves differently there: the modal-close work lands after the queued navigate and effectively supersedes it, leaving the user on the previous chat.There is no Firefox-specific guard anywhere in this navigation/modal path today (
getBrowser()exists but isn't used here).Investigation details & evidence chain
NewChatConfirmPage.tsx:215-219(onConfirm: createGroup).Navigation.dismissModalandNavigation.navigateboth route throughTransitionTracker.runAfterTransitions.TransitionTrackerflushes queued callbacks only whenactiveTransitionsreaches 0, with a 1000ms (MAX_TRANSITION_DURATION_MS) safety-net timeout — so a missed/late close callback strands or re-orders the queued navigate.onCloseCallBack→TransitionTracker.endTransitionis atReanimatedModal/index.tsx:162-179.What changes do you think we should make in order to solve the problem?
Make the navigate-to-new-report independent of the modal-close animation callback firing, so Firefox can't drop it:
navigateToReport, use the navigation API that performs dismiss + navigate atomically (e.g.Navigation.dismissModalWithReport/ passing the target report to the dismiss) instead ofdismissModal()followed by a decoupledsetTimeout(0)navigate. This removes the ordering race between the two dispatches.Container/index.web.tsxback thewithCallbackwith asetTimeout(animationOutTiming)fallback soonCloseCallBack/endTransitionfires even when the Reanimated/WAAPI completion callback doesn't fire in Firefox.What alternative solutions did you explore?
getBrowser()to skip the deferred-navigate timing — works but adds a browser special-case the codebase otherwise avoids; the atomic dismiss-and-navigate fix is preferable as it helps all engines.Open question to confirm before implementing
We should reproduce in Firefox with logging around
Container/index.web.tsx:56and theDISMISS_MODALdispatch to confirm whether (a) the exit-animation callback fails to fire, or (b) the dismiss dispatch lands after the queued navigate. Both point at the same fix area, but the repro tells us which of the two changes above is sufficient.Next Steps for Contributor+ team:
To accept:
@MelvinBot implement [this](https://github.com/Expensify/App/issues/94571)to create a draft PR.To refine:
@MelvinBot <your feedback>To reject: Explain why you are rejecting Melvin's proposal.
nabi-ebrahimi commentedon Jun 25, 2026
Proposal
What is the root cause of that problem?
The group chat itself is created correctly. The failure is in the navigation that happens right after the optimistic report is created.
NewChatConfirmPagecallsnavigateToAndCreateGroupChat(), which creates the optimistic group report and then calls:Inside
navigateToReport(), the default path does two separate things:Navigation.dismissModal()to close the new chat RHP.Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID))in asetTimeout(0).Those two operations are not actually sequenced. The code assumes the modal dismissal will be far enough along by the time the timer fires, but Firefox does not consistently resolve that ordering the same way. As a result, the group report is created and can appear in the LHN, but the visible report stays on the previously selected chat.
Relevant code paths:
App/src/pages/NewChatConfirmPage.tsx
Lines 161 to 190 in 1b3413c
App/src/libs/actions/Report/index.ts
Lines 2240 to 2265 in 1b3413c
App/src/libs/actions/Report/index.ts
Lines 2348 to 2369 in 1b3413c
What changes do you think we should make in order to solve the problem?
Update
navigateToReport()so the “dismiss modal, then open report” case uses the existing modal-aware helper instead of doing those as two separate navigation operations.When
shouldDismissModalis true andreportIDis defined, call:and return early.
Keep the current dismiss-only behavior when there is no
reportID, and keep the directNavigation.navigate()behavior for callers that passshouldDismissModal: false.This is better than manually nesting
Navigation.navigate()insidedismissModal({afterTransition})becausedismissModalWithReport()already handles the report-specific edge cases, including when the target report is already active and when the topmost route is a super-wide RHP report.It also preserves the existing
afterTransitioncallback, soclearGroupChatstill runs after the report navigation flow rather than immediately after the optimistic report is created.The existing helper already implements the sequencing we need here:
App/src/libs/Navigation/Navigation.ts
Lines 852 to 881 in 1b3413c
Add or update the
Report.navigateToAndCreateGroupChat()action test to verify that starting a group chat routes through:instead of dismissing the modal and navigating to the report independently.
We should also manually regression-test the Firefox web flow:
Start Chat -> Add to group -> Next -> Start Group
and confirm the newly created group opens.
Fixed Demo:
REC-20260625174635.mp4
What alternative solutions did you explore? (Optional)
I considered changing only
navigateToAndCreateGroupChat()to calldismissModalWithReport()directly, but the fragile sequencing is in the sharednavigateToReport()helper. Fixing the helper also covers nearby create/open-report flows that dismiss a modal before opening a report.I also considered keeping the current structure and only delaying the
Navigation.navigate()call longer, but that would still be timing-based. The code already has a helper for this exact dismiss-then-open-report flow, so we should use that instead of relying on a timer.MobileMage commentedon Jun 25, 2026
Proposal
Please re-state the problem that we are trying to solve in this issue.
In Firefox on macOS, creating a new group chat (+ → Start Chat → Add to group → Next → Start Group) does not open the newly created group. The modal closes but the user is left on whatever chat they were previously viewing instead of being taken to the new group room. This reproduces in staging and production and is specific to Firefox.
What is the root cause of that problem?
The group-creation confirm handler
createGroupinNewChatConfirmPagecallsnavigateToAndCreateGroupChat, which builds the optimistic group report and then routes the user withnavigateToReport(newChat.reportID, {afterTransition: clearGroupChat}):App/src/libs/actions/Report/index.ts
Lines 2366 to 2369 in d41e19c
Inside
navigateToReport,shouldDismissModaldefaults totrue, so it firesNavigation.dismissModal()(which dispatchesDISMISS_MODAL→StackActions.pop()to close the RHP and starts the modal-close transition), and then schedules the navigation to the new report in a baresetTimeout(0):App/src/libs/actions/Report/index.ts
Lines 2248 to 2264 in d41e19c
The two navigation dispatches are not chained — they race.
dismissModal()pops the modal and triggers atransitionStart/transitionEndcycle tracked byTransitionTracker(apopanimation drivesScreenLayout'stransitionStartlistener, incrementingactiveTransitions). Meanwhile thesetTimeout(0)callback later callsNavigation.navigate(route), andnavigateruns itslinkTowithrunImmediately = true(nowaitForTransition), so the report-push is dispatched independently of when the modal-close pop actually settles. On Chrome the modal-close pop animation and thesetTimeout(0)navigate happen to interleave such that the report push lands on the resulting state. Firefox sequences therequestAnimationFrame-driven transition events and thesetTimeout(0)macrotask differently, so the navigate fires against a navigation state that is still mid-pop; the report push is superseded/dropped by the in-flightDISMISS_MODALpop, and the user is left on the previous chat. This is the classic dismiss-then-navigate race — note the canonical helpers in the codebase never use a free-floatingsetTimeout; they chain the navigate to the dismiss completion (submitWithDismissFirstanddismissModalWithReportboth pass the navigate as the dismiss'safterTransition).What changes do you think we should make in order to solve the problem?
Stop racing the navigate against the modal dismiss inside
navigateToReport, and instead chain it to the dismiss completion exactly like the other dismiss-then-navigate flows. WhenshouldDismissModalis true, pass the report navigation asdismissModal'safterTransitioncallback so the report push only runs once theDISMISS_MODALpop transition has settled, withclearGroupChatchained after that. The existingTransitionTracker.runAfterTransitions({waitForUpcomingTransition: true})path already guarantees this fires after the pop (and has aMAX_TRANSITION_START_WAIT_MSsafety net), so no new timing primitives are needed. The core change insidenavigateToReport:This keeps the non-dismiss callers (the
shouldDismissModal: falsepaths at L2414/L2423/L2449) on the existing immediate-navigate behavior, and the existing DM/group-creation callers continue to dismiss first — only now the report push is deterministically ordered after the modal close instead of guessed viasetTimeout(0). Because the navigate is a real React Navigation dispatch (not a focus workaround), this also removes the fragile Composer-focussetTimeoutcomment justification while preserving correct ordering across Firefox, Chrome, and native.What alternative solutions did you explore? (Optional)
Bumping the
setTimeout(0)delay or wrapping the navigate inrequestAnimationFramewould paper over the Firefox timing without removing the race, and would slow the transition on every browser; chaining off the dismiss transition is both deterministic and consistent with the establishedsubmitWithDismissFirst/dismissModalWithReportpattern.x-dev90 commentedon Jun 25, 2026
Proposal
What is the root cause of that problem?
The group chat is created, but the app does not reliably navigate to it on Firefox because the group creation flow dismisses the modal and opens the new report as two separate navigation operations.
NewChatConfirmPagecallsnavigateToAndCreateGroupChat()when the user presses Start Group:App/src/pages/NewChatConfirmPage.tsx
Lines 161 to 192 in 62df01f
That action creates the optimistic group chat, then calls
navigateToReport(newChat.reportID, {afterTransition: clearGroupChat}):App/src/libs/actions/Report/index.ts
Lines 2348 to 2370 in 62df01f
The problem is inside
navigateToReport(): it first callsNavigation.dismissModal()and then schedulesNavigation.navigate(route)separately withsetTimeout(0):App/src/libs/actions/Report/index.ts
Lines 2240 to 2264 in 62df01f
On Firefox, the modal dismissal/browser history update can complete while the separate scheduled report navigation does not become the final active route. The recording confirms this: the new group appears in recents, but the URL and main pane stay on the previous Concierge report.
What changes do you think we should make in order to solve the problem?
Use the existing coordinated modal-dismiss-and-report-open helper when
navigateToReport()needs to dismiss a modal.reportIDis missing, keep the existing dismiss-only behavior.shouldDismissModalis true andreportIDexists, callNavigation.dismissModalWithReport({reportID}, undefined, {afterTransition}).shouldDismissModal: false.Tests that should be added:
Navigation.dismissModalWithReport.I considered changing
NewChatConfirmPageto navigate differently only for group chat creation, but that would keep the race-prone helper available for the same modal-to-report flow elsewhere. FixingnavigateToReport()at the shared action/navigation boundary is smaller and handles every caller that wants to dismiss a modal and open a report.What alternative solutions did you explore? (Optional)
David-2002065 commentedon Jun 25, 2026
🚨 Edited by proposal-police: This proposal was edited at 2026-06-25 12:16:45 UTC.
Proposal
Please re-state the problem that we are trying to solve in this issue.
After creating a new group chat (+ > Start Chat > Add to group > Next > Start Group), the new group chat does not open in Firefox — the user stays in the previously open chat.
What is the root cause of that problem?
Group creation ends in
navigateToReport, which dismisses the modal and navigates to the new report as two uncoordinated operations:App/src/libs/actions/Report/index.ts
Lines 2240 to 2265 in 62df01f
dismissModal()is called without itsafterTransitionhook, and thenavigate()is fired from a baresetTimeout(…, 0)with nowaitForTransition. Insidenavigate,runImmediately = !options?.waitForTransitionis thereforetrue, so the navigation runs immediately rather than queuing behind the in-flight modal-dismiss transition:App/src/libs/Navigation/Navigation.ts
Lines 331 to 341 in 62df01f
The fixed
0msmacrotask delay is a best-effort guess at "the modal has finished dismissing". It happens to be long enough on Chromium, but Firefox schedules the modal-dismiss transition differently, sonavigate()runs while the RHP dismissal is still in flight and the navigation to the new report gets dropped — leaving the user on the previous chat. This is why it reproduces only in Firefox.What changes do you think we should make in order to solve the problem?
Stop racing a
setTimeout, and instead navigate after the dismiss transition actually completes by passing the navigation as theafterTransitionofdismissModal.dismissModalalready routesafterTransitionthroughrunAfterTransitions({..., waitForUpcomingTransition: true}), which waits for the dismiss transition to start and end (with a safety fallback), so ordering is guaranteed across browsers:App/src/libs/Navigation/Navigation.ts
Lines 823 to 844 in 62df01f
In
navigateToReport:This also unifies the
reportID/no-reportIDbranches, which today already passafterTransitiontodismissModalfor the no-reportIDcase but not for thereportIDcase.Alternatively, the minimal one-line fix is to keep the existing structure but make the navigation queue behind the active dismiss transition instead of running immediately, by passing
waitForTransition: true:Note: the
setTimeout(…, 0)was originally added so the RHP modal is hidden before navigating (to avoid Composer focus breaking). Running the navigation as the dismissafterTransitionhappens strictly after the transition completes, so that concern is still satisfied — and more reliably than the fixed0msdelay.What specific scenarios should we cover in automated tests to prevent reintroducing this issue in the future?
Add a test around
navigateToReport/navigateToAndCreateGroupChatasserting thatNavigation.navigateto the new report's route is invoked afterdismissModalresolves its transition (i.e. ordering does not depend on asetTimeout(0)race), so the new report is the active route after group creation.What alternative solutions did you explore? (Optional)
The
waitForTransition: truevariant above is the smallest possible change. A heavier alternative would be replacingsetTimeout(0)withrequestAnimationFrame/InteractionManager, but that keeps the same "guess the timing" fragility that caused the Firefox divergence, rather than explicitly coordinating with the dismiss transition.aswin-s commentedon Jun 25, 2026
🚨 Edited by proposal-police: This proposal was edited at 2026-06-25 12:22:24 UTC.
Proposal
Problem
When a Firefox user creates a new group chat (+ LHN → Start Chat → Add to group → Next → Start Group), the app fails to navigate to the newly created group chat and instead leaves the user on the previous chat room. This is a regression on Firefox web only; Chrome and Safari are unaffected.
Root Cause
The bug is in
navigateToReportinsrc/libs/actions/Report/index.ts(lines 2240–2265).When called with a
reportID(the group-chat case), the function follows this path:This fires
navigate(route)while the modal dismiss animation is still in progress. In Chrome and Safari, two concurrent navigation actions are processed without conflict. In Firefox, the animation timing causes the report navigation to be dropped, leaving the user on the previous chat.There is an asymmetry inside
navigateToReportthat makes the bug visible:!reportIDbranch (line 2244): correctly passesafterTransitiontodismissModal, so the full callback is properly deferred until after the dismiss animation finishes.reportIDbranch (line 2248): callsdismissModal()with noafterTransition, then firesnavigate()immediately insetTimeout(0)— racing the dismiss animation.Secondary effect:
runAfterTransitions({callback: clearGroupChat, waitForUpcomingTransition: true})registered insidenavigatewaits for the nexttransitionStartevent (viapromiseForNextTransitionStart). In Firefox, the dismiss animation'stransitionStartmay fire afternavigate()is called, consuming the promise. This causesclearGroupChatto run after the dismiss transition rather than the report navigation transition — wrong timing, though not the primary cause of the navigation failure.Working Analog
dismissModalWithReportinsrc/libs/Navigation/Navigation.ts(lines 852–889) already solves this correctly:This chains: dismiss → wait for dismiss
transitionEnd→ then navigate → wait for report navigationtransitionEnd→ then runafterTransition.dismissModal's internal chaining (Navigation.ts:833–835) already handles this correctly:No change to
dismissModalis needed — it already chainsafterTransitioncorrectly.Fix
src/libs/actions/Report/index.ts—navigateToReportfunction (lines 2240–2265)When
shouldDismissModal && reportID, mirror thedismissModalWithReportpattern: nest thenavigatecall insidedismissModal'safterTransitioninstead of firing it concurrently:The correct chaining order is now:
dismissModal()dispatchesDISMISS_MODALtransitionStartfires →TransitionTracker.startTransition()transitionEndfires →TransitionTracker.endTransition()dismissModal'safterTransitioncallback firessetTimeout(0)→navigate(route, {afterTransition: clearGroupChat})transitionStart→transitionEndclearGroupChat()runs (clearsONYXKEYS.NEW_GROUP_CHAT_DRAFT)Callers
All callers of
navigateToReportwith a non-undefinedreportIDbenefit from this fix:navigateToAndCreateGroupChat(Report/index.ts:2369) — primary bug sitenavigateToAndOpenReport(Report/index.ts:2274)navigateToAndOpenReportWithAccountIDs(Report/index.ts:2377)shouldDismissModal: falsecallers skip the modal-dismiss path entirely and are unaffected.Alternatives Considered
MAX_TRANSITION_START_WAIT_MS— symptom workaround; slows all platforms and does not fix the root race condition.clearGroupChattodismissModal'safterTransitiondirectly — incorrect; this would clear the group chat draft after dismiss but never navigate to the new report.setTimeout(0)wrapper inside the chained call — thesetTimeout(0)comment in the original code notes it fixes Composer focus breaking when navigating immediately after an RHP modal closes; it should be preserved inside the chainedafterTransition.Abdukhamid000 commentedon Jun 25, 2026
Proposal
What is the root cause of that problem?
Creating a group chat ends in
[navigateToReport](https://github.com/Expensify/App/blob/main/src/libs/actions/Report/index.ts#L2223-L2234), which fires the modal dismiss and the destination navigation as two independent, racing operations:The flow is:
createGroup→[navigateToAndCreateGroupChat](https://github.com/Expensify/App/blob/main/src/libs/actions/Report/index.ts#L2315-L2334)→navigateToReport(newChat.reportID)with the defaultshouldDismissModal = true.Navigation.dismissModal()closes the New Chat RHP, which itself drives a navigation back to the previously open report. Separately, asetTimeout(0)schedulesNavigation.navigate(<new group chat>). These two aren't chained — the navigate is a bare macrotask that doesn't wait for the dismiss transition to finish.On Chromium/WebKit the ordering happens to land the
navigatelast, so the new chat opens. Firefox sequences the history updates and the dismiss transition differently, so the modal-dismiss's own navigation (back to the previous report) resolves after thesetTimeout(0)navigate — clobbering it. The result is exactly the reported symptom: the user is left in the previous chat room instead of the new group.The codebase already has the correct, race-free pattern for this.
[dismissModalWithReport](https://github.com/Expensify/App/blob/main/src/libs/Navigation/Navigation.ts#L824-L861)navigates inside the dismiss'safterTransitioncallback, so the navigate only fires once the modal is fully gone:navigateToReportpredates / doesn't use that helper and instead leans on the fragilesetTimeout(0), which is what breaks under Firefox's timing.What changes do you think we should make in order to solve the problem?
Chain the navigation to the dismiss completion instead of racing it on a
setTimeout. The minimal change is to usedismissModal'safterTransitioncallback innavigateToReport:function navigateToReport(reportID: string | undefined, shouldDismissModal = true) { - if (shouldDismissModal) { - Navigation.dismissModal(); - } if (!reportID) { return; } - // In some cases when RHP modal gets hidden and then we navigate to report Composer focus breaks, wrapping navigation in setTimeout fixes this - setTimeout(() => { - Navigation.isNavigationReady().then(() => Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID))); - }, 0); + const reportRoute = ROUTES.REPORT_WITH_ID.getRoute(reportID); + if (shouldDismissModal) { + // Run the navigation after the modal-dismiss transition completes instead of racing it on a + // setTimeout(0). Firefox resolves that race in favor of the dismiss's own navigation, leaving + // the user on the previous report. afterTransition also fires once the modal is fully hidden, + // which preserves the composer-focus fix the setTimeout was originally added for. + Navigation.dismissModal({afterTransition: () => Navigation.navigate(reportRoute)}); + return; + } + Navigation.isNavigationReady().then(() => Navigation.navigate(reportRoute)); }afterTransitionruns viaTransitionTracker.runAfterTransitionsafter the dismiss completes — strictly more correct thansetTimeout(0), and it preserves the composer-focus behavior the old comment was guarding (the navigate still happens after the RHP is hidden). TheshouldDismissModal = falsecallers keep navigating directly with no behavior change.A cleaner alternative is to have
navigateToReportdelegate the dismiss-and-navigate case to the existing, battle-tested[dismissModalWithReport](https://github.com/Expensify/App/blob/main/src/libs/Navigation/Navigation.ts#L824-L861), which already chains viaafterTransitionand additionally handles the "already on that report" / super-wide-RHP edge cases:if (shouldDismissModal) { - Navigation.dismissModal(); + Navigation.dismissModalWithReport({reportID}); + return; }Either way the fix is the same idea — stop racing the navigate against the dismiss. I'd lean toward delegating to
dismissModalWithReportso there's one canonical dismiss-then-open-report path and thesetTimeouthack is removed entirely. All fiveshouldDismissModal = truecall sites (group chat creation + the DM open paths) benefit, so this also fixes the same Firefox race if it shows up when opening a fresh DM.What alternative solutions did you explore?
setTimeoutdelay (e.g.setTimeout(..., 100)). Rejected — it's guessing at Firefox's transition timing; it'd be flaky across machines and still racy. Chaining on the actual transition end is deterministic.dismissModalWithReport's established order (dismiss → afterTransition navigate) is the proven sequence.melvin-bot commentedon Jun 25, 2026
Triggered auto assignment to Contributor-plus team member for initial proposal review - @nyomanjyotisa (
External)kitvasyl commentedon Jun 25, 2026
🚫 Duplicated proposal withdrawn by 🤖 ProposalPolice.
64 remaining items
melvin-bot commentedon Jul 28, 2026
Triggered auto assignment to @mallenexpensify (
Awaiting Payment)melvin-bot commentedon Jul 28, 2026
Payment Summary
Upwork Job
BugZero Checklist (@mallenexpensify)
mallenexpensify commentedon Jul 30, 2026
Payment Summary
Contributor: @emkhalid paid $250 via Upwork
Contributor+: @nyomanjyotisa due $250 via NewDot
@emkhalid can you please accept the job below? Please reply here and tag me once you have.
https://www.upwork.com/jobs/~022082910665439359059
emkhalid commentedon Jul 31, 2026
@mallenexpensify Thanks for the offer, Accepted.
trjExpensify commentedon Aug 3, 2026
approved $250 for @nyomanjyotisa
mallenexpensify commentedon Aug 4, 2026
@emkhalid paid, summary updated above. Thx.
fix: prevent state reset when history.go() takes longer than timeout …