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
[Exploratory] Web - Workspace - Transfer owner lead to not here page #101019
The "Not here" page is the success screen of the transfer-owner flow, not the check screen. DynamicWorkspaceOwnerChangeSuccessPage.tsx:41-42 latches its access decision into a useRef on the very first render:
useRef runs its initializer once and never re-evaluates. usePolicy is a bare useOnyx, and useOnyx deliberately returns undefined on a first connection when a merge is already queued for that key. So if the page mounts while policy_<id> has a pending merge, shouldShowRef.current is undefined forever, shouldBeBlocked is permanently true at line 53, and AccessOrNotFoundWrapper.tsx:227-228 renders NotFoundPage. It never recovers once the data lands.
The Workspaces-list entry point makes that race much more likely, because it writes to policy_<id> more than the other entry points do. Every requestWorkspaceOwnerChange call optimistically merges {errorFields: null, isLoading: true, isChangeOwnerSuccessful: false, isChangeOwnerFailed: false} (Member.ts:770-781) — i.e. it re-nulls exactly the two fields the success page latches on. From the Workspaces list, one flow fires it three times: TransferOwnershipFlow.tsx:44, DynamicWorkspaceOwnerChangeWrapperPage.tsx:56, and again on Continue at WorkspaceOwnerChangeCheck.tsx:74 — plus a clearWorkspaceOwnerChangeFlow merge. The Member details entry point fires it twice. The wrapper then navigates to the success route with forceReplace at line 71, mounting the success page right inside that merge window.
What changes do you think we should make in order to solve the problem?
Make the latch sticky-true instead of sticky-first-render, in both pages, so an undefined first render can't poison it:
DynamicWorkspaceOwnerChangeSuccessPage.tsx:42 — initialize the ref to false and OR the derived condition into it on every render.
DynamicWorkspaceOwnerChangeErrorPage.tsx:47 — same treatment for !policy?.errorFields && policy?.isChangeOwnerFailed.
Keeping a ref (rather than a plain derived value) is still needed: closePage calls clearWorkspaceOwnerChangeFlow, which sets isChangeOwnerSuccessful: false and would otherwise flash "Not here" on the way out.
Remove the duplicate requestWorkspaceOwnerChange at TransferOwnershipFlow.tsx:44. The wrapper page already fires it on mount, so the list entry point sends the command twice. This mirrors WorkspaceMemberDetailsPage.tsx:318-321, which only clears and navigates, and it narrows the race window.
What alternative solutions did you explore? (Optional)
Hypotheses that were tested and ruled out are in the details below.
Investigation details — what was ruled out, and how
Findings below were verified by executing the real navigation and access code in scratch Jest tests against this checkout (no repo files modified).
Rendering the real success page with Onyx seeded:
Scenario
Result
Policy settled in Onyx before mount
renders normally
Mount while an Onyx.merge on policy_<id> is pending
NotFoundPage
Same, after that merge flushes (data now correct)
NotFoundPage — never recovers
Policy key absent at mount, arrives later
NotFoundPage — never recovers
The error page behaves identically. mergeQueue[key] is created synchronously inside Onyx.merge and cleared only after an async get(), so any useOnyx connection mounting in that window reads undefined.
Ruled out:
Routing / entryScreens.workspaces/change-owner/<policyID>/<accountID>/<error>/owner-change-success resolves correctly through getStateFromPath and getAdaptedStateFromPath, passes entry-screen validation, and policyIDis populated on the success screen (inherited via getStateForDynamicRoute). Same for the error route and for the Member-details base path.
The hard-coded 'amountOwed' at TransferOwnershipFlow.tsx:47. It is a real CONST.POLICY.OWNERSHIP_ERRORS value, nothing validates the :error segment at runtime, and all three entry points hard-code the same placeholder. The wrapper overwrites it via Navigation.setParams once the API responds. Worst case it renders a momentarily blank check step, not a 404.
shouldBeBlocked on the check page. On web both platform shims are no-ops (shouldRenderTransferOwnerButton and shouldShowChangeWorkspaceOwnerPage both return true), so the check page cannot 404 on web. This matches the report: the check page rendered and the tester pressed its button.
Policy access. An admin who is not the owner passes canEditWorkspaceSettings and isGroupPolicy on an active or archived Collect/Control workspace; canBeAccessedIfArchived is already set.
Caveat: the failing mount was reproduced deterministically in a seeded render, but the production timing itself was not reproduced live — it is a race, which fits a bug that appears for some users and workspaces and not others. A one-shot confirmation: log policy at the first render of DynamicWorkspaceOwnerChangeSuccessPage during the repro; if it is undefined while Onyx holds the policy, that is the whole bug.
Secondary issue, worth fixing alongside:TransferOwnershipFlow.tsx:47 passes the current user's accountID where WorkspaceMemberDetailsPage.tsx:320 passes the owner'saccountID. That param feeds the personal-details lookup and the go-back target in WorkspaceOwnerChangeCheck.tsx, so from the Workspaces list those resolve to the wrong person.
Possibly related issues
Unable to take over ownership. — also a failing ownership transfer, but the symptom there is a server-side error rather than a "Not here" page. Likely a separate problem.
Next Steps for Contributor+ team: To accept:@MelvinBot implement [this](https://github.com/Expensify/App/issues/101019) to create a draft PR. To refine:@MelvinBot <your feedback> To reject: Explain why you are rejecting Melvin's proposal.
Please re-state the problem that we are trying to solve in this issue.
When transferring ownership of a workspace from the Workspaces tab three-dot menu, completing the transfer causes a "Not here" (404) page to appear instead of the ownership change confirmation screen.
What is the root cause of that problem?
In src/pages/workspace/members/DynamicWorkspaceOwnerChangeSuccessPage.tsx:41-42:
useRef initializes on the very first mount and never re-evaluates.
When navigating to the success page via forceReplace right after completing the transfer, policy_<id> is in the middle of active Onyx merges (requestWorkspaceOwnerChange / clearWorkspaceOwnerChangeFlow).
When a key has an active merge in progress, usePolicy (useOnyx) initial connection returns undefined.
Consequently, shouldShowRef.current evaluates to false on the initial mount and stays permanently false.
At line 53, shouldBeBlocked={!shouldShowRef.current} becomes permanently true, causing AccessOrNotFoundWrapper to render NotFoundPage ("Not here" page).
DynamicWorkspaceOwnerChangeErrorPage.tsx:47 suffers from the identical issue (shouldShowRef = useRef(!policy?.errorFields && policy?.isChangeOwnerFailed)).
Additionally, in TransferOwnershipFlow.tsx:44, requestWorkspaceOwnerChange is redundantly invoked prior to navigation, while DynamicWorkspaceOwnerChangeWrapperPage already triggers it on mount, causing duplicate server commands and extending the merge window.
What changes do you think we should make in order to solve the problem?
Make shouldShowRef sticky-true in DynamicWorkspaceOwnerChangeSuccessPage.tsx and DynamicWorkspaceOwnerChangeErrorPage.tsx:
In DynamicWorkspaceOwnerChangeSuccessPage.tsx:
(Retaining the ref ensures that when closePage calls clearWorkspaceOwnerChangeFlow to reset isChangeOwnerSuccessful: false, the screen does not flash "Not here" while animating out).
Deduplicate requestWorkspaceOwnerChange in TransferOwnershipFlow.tsx:
Remove line 44 (requestWorkspaceOwnerChange(...)) from TransferOwnershipFlow.tsx, matching WorkspaceMemberDetailsPage.tsx:318-321 which clears and navigates, letting the wrapper page manage the API request on mount. Also pass the workspace owner's accountID (policy?.ownerAccountID ?? currentUserPersonalDetails.accountID) so the back-path parameter correctly resolves to the former owner.
What alternative solutions did you explore? (Optional)
Using a purely dynamic boolean without a ref was considered, but closePage() immediately invokes clearWorkspaceOwnerChangeFlow(policyID) which clears isChangeOwnerSuccessful before navigation unmounts, creating an exit flash. A sticky-true latch guarantees that once success/error is verified from Onyx data, the page remains accessible through transition and dismissal.
Activity
applause-bot commentedon Sep 12, 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/3eef47e8be4e2a596b5b0e0b87535abc27fa33475b254c1155841405d673087f
MelvinBot commentedon Sep 24, 2026
Proposal
What is the root cause of that problem?
The "Not here" page is the success screen of the transfer-owner flow, not the check screen.
DynamicWorkspaceOwnerChangeSuccessPage.tsx:41-42latches its access decision into auseRefon the very first render:useRefruns its initializer once and never re-evaluates.usePolicyis a bareuseOnyx, anduseOnyxdeliberately returnsundefinedon a first connection when a merge is already queued for that key. So if the page mounts whilepolicy_<id>has a pending merge,shouldShowRef.currentisundefinedforever,shouldBeBlockedis permanentlytrueat line 53, andAccessOrNotFoundWrapper.tsx:227-228rendersNotFoundPage. It never recovers once the data lands.The Workspaces-list entry point makes that race much more likely, because it writes to
policy_<id>more than the other entry points do. EveryrequestWorkspaceOwnerChangecall optimistically merges{errorFields: null, isLoading: true, isChangeOwnerSuccessful: false, isChangeOwnerFailed: false}(Member.ts:770-781) — i.e. it re-nulls exactly the two fields the success page latches on. From the Workspaces list, one flow fires it three times:TransferOwnershipFlow.tsx:44,DynamicWorkspaceOwnerChangeWrapperPage.tsx:56, and again on Continue atWorkspaceOwnerChangeCheck.tsx:74— plus aclearWorkspaceOwnerChangeFlowmerge. The Member details entry point fires it twice. The wrapper then navigates to the success route withforceReplaceat line 71, mounting the success page right inside that merge window.DynamicWorkspaceOwnerChangeErrorPage.tsx:46-47has the identical defect.What changes do you think we should make in order to solve the problem?
Make the latch sticky-true instead of sticky-first-render, in both pages, so an
undefinedfirst render can't poison it:DynamicWorkspaceOwnerChangeSuccessPage.tsx:42— initialize the ref tofalseand OR the derived condition into it on every render.DynamicWorkspaceOwnerChangeErrorPage.tsx:47— same treatment for!policy?.errorFields && policy?.isChangeOwnerFailed.Keeping a ref (rather than a plain derived value) is still needed:
closePagecallsclearWorkspaceOwnerChangeFlow, which setsisChangeOwnerSuccessful: falseand would otherwise flash "Not here" on the way out.Remove the duplicate
requestWorkspaceOwnerChangeatTransferOwnershipFlow.tsx:44. The wrapper page already fires it on mount, so the list entry point sends the command twice. This mirrorsWorkspaceMemberDetailsPage.tsx:318-321, which only clears and navigates, and it narrows the race window.What alternative solutions did you explore? (Optional)
Hypotheses that were tested and ruled out are in the details below.
Investigation details — what was ruled out, and how
Findings below were verified by executing the real navigation and access code in scratch Jest tests against this checkout (no repo files modified).
Rendering the real success page with Onyx seeded:
Onyx.mergeonpolicy_<id>is pendingThe error page behaves identically.
mergeQueue[key]is created synchronously insideOnyx.mergeand cleared only after an asyncget(), so anyuseOnyxconnection mounting in that window readsundefined.Ruled out:
entryScreens.workspaces/change-owner/<policyID>/<accountID>/<error>/owner-change-successresolves correctly throughgetStateFromPathandgetAdaptedStateFromPath, passes entry-screen validation, andpolicyIDis populated on the success screen (inherited viagetStateForDynamicRoute). Same for the error route and for the Member-details base path.'amountOwed'atTransferOwnershipFlow.tsx:47. It is a realCONST.POLICY.OWNERSHIP_ERRORSvalue, nothing validates the:errorsegment at runtime, and all three entry points hard-code the same placeholder. The wrapper overwrites it viaNavigation.setParamsonce the API responds. Worst case it renders a momentarily blank check step, not a 404.shouldBeBlockedon the check page. On web both platform shims are no-ops (shouldRenderTransferOwnerButtonandshouldShowChangeWorkspaceOwnerPageboth returntrue), so the check page cannot 404 on web. This matches the report: the check page rendered and the tester pressed its button.canEditWorkspaceSettingsandisGroupPolicyon an active or archived Collect/Control workspace;canBeAccessedIfArchivedis already set.Caveat: the failing mount was reproduced deterministically in a seeded render, but the production timing itself was not reproduced live — it is a race, which fits a bug that appears for some users and workspaces and not others. A one-shot confirmation: log
policyat the first render ofDynamicWorkspaceOwnerChangeSuccessPageduring the repro; if it isundefinedwhile Onyx holds the policy, that is the whole bug.Secondary issue, worth fixing alongside:
TransferOwnershipFlow.tsx:47passes the current user'saccountIDwhereWorkspaceMemberDetailsPage.tsx:320passes the owner'saccountID. That param feeds the personal-details lookup and the go-back target inWorkspaceOwnerChangeCheck.tsx, so from the Workspaces list those resolve to the wrong person.Possibly related issues
Next Steps for Contributor+ team:
To accept:
@MelvinBot implement [this](https://github.com/Expensify/App/issues/101019)to create a draft PR.To refine:
@MelvinBot <your feedback>To reject: Explain why you are rejecting Melvin's proposal.
melvin-bot commentedon Sep 24, 2026
Triggered auto assignment to Contributor-plus team member for initial proposal review - @abdulrahuman5196 (
External)melvin-bot commentedon Sep 24, 2026
Unable to auto-create job on Upwork. The BZ team member should create it manually for this issue.
ashusatyarthi-wq commentedon Sep 26, 2026
Contributor details
Your Expensify account email: ashu.satyarthi.business@gmail.com Upwork Profile Link: https://www.upwork.com/freelancers/~01b8ab177d61dc225cProposal
Please re-state the problem that we are trying to solve in this issue.
When transferring ownership of a workspace from the Workspaces tab three-dot menu, completing the transfer causes a "Not here" (404) page to appear instead of the ownership change confirmation screen.
What is the root cause of that problem?
In
src/pages/workspace/members/DynamicWorkspaceOwnerChangeSuccessPage.tsx:41-42:useRefinitializes on the very first mount and never re-evaluates.forceReplaceright after completing the transfer,policy_<id>is in the middle of active Onyx merges (requestWorkspaceOwnerChange/clearWorkspaceOwnerChangeFlow).usePolicy(useOnyx) initial connection returnsundefined.shouldShowRef.currentevaluates tofalseon the initial mount and stays permanentlyfalse.shouldBeBlocked={!shouldShowRef.current}becomes permanentlytrue, causingAccessOrNotFoundWrapperto renderNotFoundPage("Not here" page).DynamicWorkspaceOwnerChangeErrorPage.tsx:47suffers from the identical issue (shouldShowRef = useRef(!policy?.errorFields && policy?.isChangeOwnerFailed)).TransferOwnershipFlow.tsx:44,requestWorkspaceOwnerChangeis redundantly invoked prior to navigation, whileDynamicWorkspaceOwnerChangeWrapperPagealready triggers it on mount, causing duplicate server commands and extending the merge window.What changes do you think we should make in order to solve the problem?
Make
shouldShowRefsticky-true inDynamicWorkspaceOwnerChangeSuccessPage.tsxandDynamicWorkspaceOwnerChangeErrorPage.tsx:In
DynamicWorkspaceOwnerChangeSuccessPage.tsx:(Retaining the ref ensures that when
closePagecallsclearWorkspaceOwnerChangeFlowto resetisChangeOwnerSuccessful: false, the screen does not flash "Not here" while animating out).In
DynamicWorkspaceOwnerChangeErrorPage.tsx:Deduplicate
requestWorkspaceOwnerChangeinTransferOwnershipFlow.tsx:Remove line 44 (
requestWorkspaceOwnerChange(...)) fromTransferOwnershipFlow.tsx, matchingWorkspaceMemberDetailsPage.tsx:318-321which clears and navigates, letting the wrapper page manage the API request on mount. Also pass the workspace owner's accountID (policy?.ownerAccountID ?? currentUserPersonalDetails.accountID) so the back-path parameter correctly resolves to the former owner.What alternative solutions did you explore? (Optional)
Using a purely dynamic boolean without a ref was considered, but
closePage()immediately invokesclearWorkspaceOwnerChangeFlow(policyID)which clearsisChangeOwnerSuccessfulbefore navigation unmounts, creating an exit flash. A sticky-true latch guarantees that once success/error is verified from Onyx data, the page remains accessible through transition and dismissal.15 remaining items
melvin-bot commentedon Oct 22, 2026
This issue has not been updated in over 14 days. @abdulrahuman5196 eroding to Weekly issue.
melvin-bot commentedon Nov 3, 2026
@abdulrahuman5196 Huh... This is 4 days overdue. Who can take care of this?
melvin-bot commentedon Nov 3, 2026
@abdulrahuman5196 Huh... This is 4 days overdue. Who can take care of this?
mukhrr commentedon Nov 3, 2026
I think this is now fixed by #100501
melvin-bot commentedon Nov 3, 2026
@abdulrahuman5196 Huh... This is 4 days overdue. Who can take care of this?
melvin-bot commentedon Nov 4, 2026
@abdulrahuman5196 Eep! 4 days overdue now. Issues have feelings too...
melvin-bot commentedon Nov 5, 2026
@abdulrahuman5196 6 days overdue. This is scarier than being forced to listen to Vogon poetry!
jeffrey-graham commentedon Nov 7, 2026
melvin-bot commentedon Nov 7, 2026
melvin-bot commentedon Nov 9, 2026
@abdulrahuman5196 10 days overdue. Is anyone even seeing these? Hello?
melvin-bot commentedon Nov 11, 2026
@abdulrahuman5196 12 days overdue. Walking. Toward. The. Light...