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-10-12] Workspace - Back button reopens upgrade success after refreshing upgrade page and upgrade #102184
Version Number: 9.4.93-0 Reproducible in staging?: Yes Reproducible in production?: N/A - new feature, doesn't exist in prod If this was caught during regression testing, add the test name, ID and link from BrowserStack:#101710 Email or phone of affected tester (no customers):jsdoijnodsdisojo@gmail.com Issue reported by: Applause Internal Team Bug source: Exploratory - Significant User Experience Deterioration Device used: Mac 26.5 / Chrome App Component: Workspace Settings
Action Performed:
Precondition:
Create two workspaces - A and B.
Workspace A is Control workspace.
Workspace B is Collect workspace.
For workspace A, enable Rules and enable attendee tracking.
Go to staging.new.expensify.com
2 Go to Workspaces.
👋 Friendly reminder that deploy blockers are time-sensitive ⏱ issues! Check out the open `StagingDeployCash` deploy checklist to see the list of PRs included in this release, then work quickly to do one of the following:
Identify the pull request that introduced this issue and revert it.
The defective code is CopyPolicySettingsUpgradePage, which is byte-identical on staging and production, so there is nothing on this checklist to revert — reverting #101710 would only hide one entry point while reintroducing the Control→Collect Rules copy failure it fixed. The fix is a one-line change: acknowledge the upgrade with Navigation.navigate(..., {forceReplace: true}) so the completed Upgrade step leaves the stack.
Assigned: @fedirjh (PR author), @shubham1206agra and @yuwenmemon (approving reviewers) from #94060, the highest-confidence causing PR Labels: No changes. DeployBlockerCash stays (App-side fix, and the issue is not confirmed non-reproducible in production — see below). DeployBlocker was never applied.
📋 Detailed Analysis
Evidence
#101710 did not introduce the defect. Its entire diff is 16 added / 3 removed lines in src/libs/CopyPolicySettingsUtils.ts plus test updates. It adds one early return so rules and codingRules count as Control-only for a Collect target, which routes Select settings to the Upgrade step instead of straight to Confirm. It touches no navigation code, no page component and no local state.
The broken screen is unchanged from production. Diffing production...main for the whole copy-settings area returns no change at all to CopyPolicySettingsUpgradePage.tsx, CopyPolicySettingsConfirmPage.tsx, CopyPolicySettingsSelectWorkspacesPage.tsx, the copy-settings routes in linkingConfig/config.ts, or Navigation.ts. The only staging-only changes in that area are #101710's gating lines and an unrelated member-count selector tweak in the Select features page.
The Upgrade step is already reachable in production, so this should reproduce there.canPolicyAccessFeature is identical on production and staging and treats ARE_PER_DIEM_RATES_ENABLED and ARE_INVOICE_FIELDS_ENABLED as Control-only. Via PART_TO_POLICY_FEATURE that means Per diem (and Invoices when the source has invoice fields) already routes a Collect target through the same Upgrade step on production today. The same steps — copy Per diem from a Control workspace to a Collect workspace, refresh the Upgrade RHP, upgrade, then press Back three times — should reproduce this on production. The "N/A — new feature" note in the report is true only of the Rules path, not of the Upgrade step itself. Worth re-testing that path before treating this as staging-only.
The copy-settings entry point is not beta gated (WorkspaceRowThreeDotsMenu only checks isAdmin and isEligibleToCopy), so nothing else keeps this off production.
Files changed by #101710: src/libs/CopyPolicySettingsUtils.ts and tests/unit/CopyPolicySettingsUtilsTest.ts only. It does not modify the affected file, which is why the auto-assignment is refuted.
Root Cause
The Upgrade step keeps its "the user already upgraded" flag in component state (hasRequestedUpgrade, L67) and then moves forward with a push (navigateToConfirm, L102-L107), so the completed, non-repeatable step stays mounted underneath the Confirm screen with showSuccess latched to true.
The refresh in step 6 is what makes that stale screen reachable. On a refresh, getAdaptedStateFromPath only prepends the full-screen route under the RHP; it does not synthesize the earlier wizard steps, so the copy-settings stack is just [Upgrade]. From there:
Continue pushes Confirm → [Upgrade, Confirm], with Upgrade still mounted and hasRequestedUpgrade === true.
Back Add Expensify eslint #3 on Select workspaces is a plain Navigation.goBack with no fallback, so it pops onto the stale Upgrade screen instead of closing the RHP. hasRequestedUpgrade is still true and policiesToUpgrade is now empty (the targets are Control), so showSuccess is true and "You've upgraded your workspace!" reappears — with shouldShowBackButton={!showSuccess} hiding the back button, dead-ending the pane.
Without the refresh, Select features is in the stack, so Back #1 pops two screens at once and the Upgrade screen never survives — which is exactly why step 6 is a required precondition.
The redirect effect at L85-L90, whose comment says it exists to handle "back navigation after upgrading", cannot help: it returns early when hasRequestedUpgrade is true, which is precisely this case.
Suggested fix (one line, in navigateToConfirm), matching the pattern WorkspaceUpgradePage already uses for its own post-upgrade hop:
// The upgrade is one-way, so replace this step instead of pushing on top of it. Otherwise a// refreshed stack leaves it underneath and Back lands on the stale success view.Navigation.navigate(ROUTES.POLICY_COPY_SETTINGS_CONFIRM.getRoute(sourcePolicyID),{forceReplace: true});
Regression check for the fix: after upgrading, Back from Confirm should reach Select features (or close the RHP when it was deep-linked), and re-pressing Next there should go straight to Confirm, since the targets are now Control.
💡 Suggested AI code review rule
This bug looks like a recurring, mechanically detectable pattern, so it may be worth a coding-standards rule so the AI reviewer catches the next one. Proposed for Expensify/App (.claude/skills/app-coding-standards):
Rule:UI-4 Leave one-way flow steps behind with forceReplace
Condition: When a wizard/RHP step is one-way (upgrade completed, payment taken, success acknowledged) and latches that fact in local useState, move to the next step with Navigation.navigate(route, {forceReplace: true}) instead of a plain push. A screen that hides its own back button in a terminal state (shouldShowBackButton={!someTerminalState}) must not stay in the stack.
Why: A refresh or deep link rebuilds the RHP with only the current screen, so goBack(fallback) REPLACEs rather than pops; the completed step lingers underneath, and a later Back re-renders its latched success view with no way out. Incident: this deploy blocker.
Search pattern:rg -n "shouldShowBackButton=\{!" src/pages and rg -n -B2 -A6 "afterUpgradeAcknowledged|onSuccess|navigateTo(Confirm|Next)" src/pages | rg -v "forceReplace" — flag forward navigation out of a terminal state that lacks {forceReplace: true}.
The full rule file, with the ❌/✅ code inline in App's coding-standards format, would go in Expensify/App itself. If it's worth adding, a maintainer can open it there as a draft PR.
Wouldn't it be better to assign people after the more detailed review? As on this issue where we now have to pay a C+ vs having the reviewer of the causing PR? What's downside with doing away with the first post? It was only posted 13 minutes before so I don't think time is the issue.
The two comments come from two separate bots, and neither one cleans up after the other.
First comment (melvin-bot, 13:42): This is a rule, not an investigation. It assigns the merger and reviewers of whatever PR the tester put in the "caught during regression testing" field. That field pointed to Show the Upgrade step when copying rules to a Collect workspace, so marcochavezf, JmillsExpensify, and daledah were assigned.
Second comment (MelvinBot, 13:55): This one reads the code. It found that [Bulk workspace edits] Release 2: remove Control-only restriction + Upgrade page caused the bug, and it assigned that PR's author and reviewers. But it only adds assignees. It never removed the people from the first comment, so daledah stayed on, reviewed the fix, and is now owed payment.
I agree that the real gap is the missing unassign step, not the timing.
Dropping the first comment has one downside. MelvinBot only assigns when it reaches medium or high confidence. If an investigation fails or can't pin down a PR, nobody would own the blocker. Two fixes would avoid that:
When MelvinBot rules out the linked PR with high confidence, it removes the people the first comment assigned.
Or the first comment waits for MelvinBot and assigns its people only as a fallback.
Either fix needs an internal change. I can't make it from this issue.
Activity
applause-bot commentedon Sep 24, 2026
While Applause doesn't embed attachments for this bug, here are the links for quick view:
https://platform.applause.com/services/links/v1/external/7262dbecacd183531819db5d4c28d3ee5e6154e7aa2dff0924051889322ac373
melvin-bot commentedon Sep 24, 2026
You have been assigned to this deploy blocker because you recently merged this PR: #101710
melvin-bot commentedon Sep 24, 2026
@marcochavezf @JmillsExpensify @daledah you have been assigned to this deploy blocker because you reviewed the offending PR: #101710
melvin-bot commentedon Sep 24, 2026
💬 A slack conversation has been started in #expensify-open-source
github-actions commentedon Sep 24, 2026
👋 Friendly reminder that deploy blockers are time-sensitive ⏱ issues! Check out the open `StagingDeployCash` deploy checklist to see the list of PRs included in this release, then work quickly to do one of the following:
MelvinBot commentedon Sep 24, 2026
🔍 Investigation Summary
Classification: Frontend bug
Causing PR: [Bulk workspace edits] Release 2: remove Control-only restriction + Upgrade page (
Expensify/App#94060) by @fedirjh (High confidence) — merged 2026-07-08, already in productionNot the cause: Show the Upgrade step when copying rules to a Collect workspace (
Expensify/App#101710) by @daledah — refuted below (High confidence). It only made an existing broken screen reachable from one more entry point.Related Issues: Copying "Rules" from Control to Collect fails instead of offering the Upgrade step (
Expensify/App#101456) — the issue#101710fixedRecommendation: ROLL FORWARD
The defective code is
CopyPolicySettingsUpgradePage, which is byte-identical on staging and production, so there is nothing on this checklist to revert — reverting#101710would only hide one entry point while reintroducing the Control→Collect Rules copy failure it fixed. The fix is a one-line change: acknowledge the upgrade withNavigation.navigate(..., {forceReplace: true})so the completed Upgrade step leaves the stack.Assigned: @fedirjh (PR author), @shubham1206agra and @yuwenmemon (approving reviewers) from
#94060, the highest-confidence causing PRLabels: No changes.
DeployBlockerCashstays (App-side fix, and the issue is not confirmed non-reproducible in production — see below).DeployBlockerwas never applied.📋 Detailed Analysis
Evidence
#101710did not introduce the defect. Its entire diff is 16 added / 3 removed lines insrc/libs/CopyPolicySettingsUtils.tsplus test updates. It adds one early return sorulesandcodingRulescount as Control-only for a Collect target, which routes Select settings to the Upgrade step instead of straight to Confirm. It touches no navigation code, no page component and no local state.The broken screen is unchanged from production. Diffing
production...mainfor the whole copy-settings area returns no change at all toCopyPolicySettingsUpgradePage.tsx,CopyPolicySettingsConfirmPage.tsx,CopyPolicySettingsSelectWorkspacesPage.tsx, the copy-settings routes inlinkingConfig/config.ts, orNavigation.ts. The only staging-only changes in that area are#101710's gating lines and an unrelated member-count selector tweak in the Select features page.The Upgrade step is already reachable in production, so this should reproduce there.
canPolicyAccessFeatureis identical on production and staging and treatsARE_PER_DIEM_RATES_ENABLEDandARE_INVOICE_FIELDS_ENABLEDas Control-only. ViaPART_TO_POLICY_FEATUREthat means Per diem (and Invoices when the source has invoice fields) already routes a Collect target through the same Upgrade step on production today. The same steps — copy Per diem from a Control workspace to a Collect workspace, refresh the Upgrade RHP, upgrade, then press Back three times — should reproduce this on production. The "N/A — new feature" note in the report is true only of the Rules path, not of the Upgrade step itself. Worth re-testing that path before treating this as staging-only.The copy-settings entry point is not beta gated (
WorkspaceRowThreeDotsMenuonly checksisAdminandisEligibleToCopy), so nothing else keeps this off production.Verification
Affected file:
src/pages/workspace/copyPolicySettings/CopyPolicySettingsUpgradePage.tsx, added by#94060and unmodified since (only two follow-ups, a label-fallback cleanup and a flicker fix, have touched it — neither changed the navigation or thehasRequestedUpgradestate).Files changed by
#101710:src/libs/CopyPolicySettingsUtils.tsandtests/unit/CopyPolicySettingsUtilsTest.tsonly. It does not modify the affected file, which is why the auto-assignment is refuted.Root Cause
The Upgrade step keeps its "the user already upgraded" flag in component state (
hasRequestedUpgrade, L67) and then moves forward with a push (navigateToConfirm, L102-L107), so the completed, non-repeatable step stays mounted underneath the Confirm screen withshowSuccesslatched totrue.The refresh in step 6 is what makes that stale screen reachable. On a refresh,
getAdaptedStateFromPathonly prepends the full-screen route under the RHP; it does not synthesize the earlier wizard steps, so the copy-settings stack is just[Upgrade]. From there:[Upgrade, Confirm], with Upgrade still mounted andhasRequestedUpgrade === true.goBack(POLICY_COPY_SETTINGS_SELECT_FEATURES). Select features is not in the rebuilt stack, socanPopToRouteis false andgoUpdispatches a REPLACE →[Upgrade, Select features].goBack(POLICY_COPY_SETTINGS)→ same REPLACE →[Upgrade, Select workspaces].Navigation.goBackwith no fallback, so it pops onto the stale Upgrade screen instead of closing the RHP.hasRequestedUpgradeis stilltrueandpoliciesToUpgradeis now empty (the targets are Control), soshowSuccessistrueand "You've upgraded your workspace!" reappears — withshouldShowBackButton={!showSuccess}hiding the back button, dead-ending the pane.Without the refresh, Select features is in the stack, so Back #1 pops two screens at once and the Upgrade screen never survives — which is exactly why step 6 is a required precondition.
The redirect effect at L85-L90, whose comment says it exists to handle "back navigation after upgrading", cannot help: it returns early when
hasRequestedUpgradeis true, which is precisely this case.Suggested fix (one line, in
navigateToConfirm), matching the patternWorkspaceUpgradePagealready uses for its own post-upgrade hop:Regression check for the fix: after upgrading, Back from Confirm should reach Select features (or close the RHP when it was deep-linked), and re-pressing Next there should go straight to Confirm, since the targets are now Control.
💡 Suggested AI code review rule
This bug looks like a recurring, mechanically detectable pattern, so it may be worth a coding-standards rule so the AI reviewer catches the next one. Proposed for
Expensify/App(.claude/skills/app-coding-standards):UI-4Leave one-way flow steps behind withforceReplaceuseState, move to the next step withNavigation.navigate(route, {forceReplace: true})instead of a plain push. A screen that hides its own back button in a terminal state (shouldShowBackButton={!someTerminalState}) must not stay in the stack.goBack(fallback)REPLACEs rather than pops; the completed step lingers underneath, and a later Back re-renders its latched success view with no way out. Incident: this deploy blocker.rg -n "shouldShowBackButton=\{!" src/pagesandrg -n -B2 -A6 "afterUpgradeAcknowledged|onSuccess|navigateTo(Confirm|Next)" src/pages | rg -v "forceReplace"— flag forward navigation out of a terminal state that lacks{forceReplace: true}.App/src/pages/workspace/copyPolicySettings/CopyPolicySettingsUpgradePage.tsx
Lines 102 to 107 in 1e3ca2f
{forceReplace: true}, as inApp/src/pages/workspace/upgrade/WorkspaceUpgradePage.tsx
Lines 197 to 205 in 1e3ca2f
The full rule file, with the ❌/✅ code inline in App's coding-standards format, would go in
Expensify/Appitself. If it's worth adding, a maintainer can open it there as a draft PR.27 remaining items
melvin-bot commentedon Oct 11, 2026
Triggered auto assignment to @mallenexpensify (
Awaiting Payment)melvin-bot commentedon Oct 11, 2026
Payment Summary
Resolving PRs:
Fix back navigation after workspace copy upgrade #102538
Contributor: @fedirjh does not require payment (Contractor)
Reviewer: @shubham1206agra owed $175 via NewDot
Reviewer: @daledah owed $175 via NewDot
BugZero Checklist (@mallenexpensify)
mallenexpensify commentedon Oct 14, 2026
@MelvinBot explain the difference between this comment, which comes first from @melvin-bot
And the second review from @MelvinBot ?
Wouldn't it be better to assign people after the more detailed review? As on this issue where we now have to pay a C+ vs having the reviewer of the causing PR? What's downside with doing away with the first post? It was only posted 13 minutes before so I don't think time is the issue.
MelvinBot commentedon Oct 14, 2026
The two comments come from two separate bots, and neither one cleans up after the other.
melvin-bot, 13:42): This is a rule, not an investigation. It assigns the merger and reviewers of whatever PR the tester put in the "caught during regression testing" field. That field pointed to Show the Upgrade step when copying rules to a Collect workspace, somarcochavezf,JmillsExpensify, anddaledahwere assigned.MelvinBot, 13:55): This one reads the code. It found that [Bulk workspace edits] Release 2: remove Control-only restriction + Upgrade page caused the bug, and it assigned that PR's author and reviewers. But it only adds assignees. It never removed the people from the first comment, sodaledahstayed on, reviewed the fix, and is now owed payment.I agree that the real gap is the missing unassign step, not the timing.
Dropping the first comment has one downside.
MelvinBotonly assigns when it reaches medium or high confidence. If an investigation fails or can't pin down a PR, nobody would own the blocker. Two fixes would avoid that:MelvinBotrules out the linked PR with high confidence, it removes the people the first comment assigned.MelvinBotand assigns its people only as a fallback.Either fix needs an internal change. I can't make it from this issue.
view run
mallenexpensify commentedon Oct 14, 2026
Payment Summary
Contributor+: @daledah due $175 via NewDot
quinthar commentedon Oct 27, 2026
Approved $175 to @daledah