Skip to content

[$175] WS settings - Error appear when changing role just after adding User #103058

Open

Description

@applause-bot

If you haven’t already, check out our contributing guidelines for onboarding. To join our Slack channel, fill out this form.


Version Number: 9.5.2-0
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://test-management.browserstack.com/projects/2219752/test-runs/77022182/folder/54013438/41237088/1825018507
Email or phone of affected tester (no customers): [email protected]
Issue reported by: Applause Internal Team
Bug source: Regression TC Execution
Device used: Windows 11 / Chrome
App Component: Workspace Settings

Action Performed:

  1. Open staging.new.expensify
  2. Go to WS settings > members > invite a non-existing expensify user
  3. Click on newly added user
  4. Click on Role

Expected Result:

Error should not appear.

Actual Result:

Error appear when user click on Role quickly after adding the User

Workaround:

Unknown

Platforms:

  • Android: App
  • Android: mWeb Chrome
  • iOS: App
  • iOS: mWeb Safari
  • iOS: mWeb Chrome
  • Windows: Chrome
  • MacOS: Chrome Safari

Screenshots/Videos

Bug7277483_1791180954584.Recording__1229.mp4

View all open jobs on GitHub

Applause Internal Information
Exported by: Rizky Gunawan
Bug ID: 7277483

Upwork Automation - Do Not Edit

Activity

  1. applause-bot commented on Oct 5, 2026

    @applause-bot
    Author

    While Applause doesn't embed attachments for this bug, here are the links for quick view:
    https://platform.applause.com/services/links/v1/external/31c2ac10b6fadfcbe2cb7d516a568ab917ee4aff045b4a40f971cfd5be9384bf

  2. added
    BugSomething is broken. Auto assigns a BugZero manager.
    on Oct 5, 2026
  3. MelvinBot commented on Oct 5, 2026

    @MelvinBot
    Contributor

    Proposal

    What is the root cause of that problem?

    The Role page finds the member using the accountID in its URL. For a brand-new invitee, that ID is a temporary (optimistic) ID. When the AddMembersToWorkspace response arrives, the app deletes the temporary personal-details entry. The Role page then can't find the member's login or role. It blocks itself and shows the "Hmm... it's not here" page.

    The member details page already handles this case. Keep workspace member details resolvable after an invite completes added getMemberLoginByOptimisticAccountID (WorkspaceMemberDetailsPage.tsx:95, used at :131). That fix never reached the Role page.

    What changes do you think we should make in order to solve the problem?

    1. Move getMemberLoginByOptimisticAccountID from WorkspaceMemberDetailsPage.tsx into a shared util, for example PolicyUtils.ts.
    2. In WorkspaceMemberDetailsRolePage, resolve the member the same way the details page does:
      • Fall back to that helper when the personal-details lookup fails.
      • Get the real accountID with usePersonalDetailByLogin(memberLogin).
      • Pass the resolved accountID to updateWorkspaceMembersRole, isRuleBotEnforcingRules, and the back routes.
    3. Apply the same lookup to two pages that use the same broken pattern:
    4. Add a Role page UI test modeled on the stale optimistic accountID test in tests/ui/WorkspaceMemberDetailsPageTest.tsx.

    What alternative solutions did you explore? (Optional)

    Make pending-invite rows non-clickable, or disable the Role row while pendingAction === ADD. The members table already sets isInteractive: !details.isOptimisticPersonalDetail (WorkspaceMembersPage.tsx:447). The new table row from Update the workspace members table to the new style ignores that flag. This alone doesn't help with deep links, refreshes, or slow-loading real personal details, so it should only supplement the fix above.

    Reproduction attempt and confidence
    • Confidence: medium. The code path is confirmed. The error was not reproduced on a local web dev build.
    • Fast path: On the local dev server, the invite request finished before the Role page opened, so the race never happened.
    • Offline path: We invited a member while offline and opened the Role page while the invite was pending. The page rendered fine because the temporary entry still existed. We then left the Role page before going back online. The failing case needs the Role page to stay open while the response arrives, and that wasn't tested.
    • Not a backend rejection: The report only needs a click on Role, with no role selected. A failed UpdateWorkspaceMembersRole would show workspace.editor.genericFailureMessage instead.
    • Related: [Due for payment 2026-07-29] [$250] WS settings - Invite member from WS settings then clicking on it gives error in RHP is the same race on the member details page. It is fixed and closed.

    Next Steps for Contributor+ team:
    To accept: @MelvinBot implement [this](https://github.com/Expensify/App/issues/103058) to create a draft PR.
    To refine: @MelvinBot <your feedback>
    To reject: Explain why you are rejecting Melvin's proposal.


    view run · view recording

  4. added
    ExternalAdded to denote the issue can be worked on by a contributor
    on Oct 5, 2026
  5. melvin-bot commented on Oct 5, 2026

    @melvin-bot

    Triggered auto assignment to Contributor-plus team member for initial proposal review - @marufsharifi (External)

  6. changed the title [-]WS settings - Error appear when changing role just after adding User[/-] [+][$175] WS settings - Error appear when changing role just after adding User[/+] on Oct 5, 2026
  7. melvin-bot commented on Oct 5, 2026

    @melvin-bot
  8. camesenin commented on Oct 5, 2026

    @camesenin

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    In Workspace Settings > Members, immediately after inviting a new member, if an admin opens that member's details and taps "Role" to change their role while the invite response settles, the page displays the "Hmm... it's not here" (not found / blocked) screen instead of the role selection list.

    What is the root cause of that problem?

    1. When inviting a new user to a workspace, the app optimistically generates an account ID (generateAccountID(login)). The navigation route to member details and subsequent subpages (including Role) receives this optimistic accountID in route.params.
    2. When the backend AddMembersToWorkspace API response resolves, Onyx updates the member's data with their permanent account ID, and optimistic personal details are removed from personalDetails.
    3. In src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx:139, PR #96449 introduced getMemberLoginByOptimisticAccountID(policy, routeAccountID) and usePersonalDetailByLogin(memberLogin) so the parent details page can resolve the member login and permanent account ID across this optimistic-to-permanent ID swap.
    4. However, in src/pages/workspace/members/WorkspaceMemberDetailsRolePage.tsx:39-41:
      const memberLogin = personalDetails?.[accountID]?.login ?? '';
      const member = policy?.employeeList?.[memberLogin];
      const canManageSelectedMemberRole = canMemberAssignRole(policy, currentUserLogin, member?.role);
      The Role page only attempts a direct lookup in personalDetails?.[accountID]?.login. Once the optimistic personal details entry is cleared, memberLogin falls back to '', member becomes undefined, and canMemberAssignRole returns false.
    5. Because canManageSelectedMemberRole is false, WorkspaceMemberDetailsRolePage.tsx:76 activates shouldBeBlocked={!canManageSelectedMemberRole} inside AccessOrNotFoundWrapper, triggering the "Hmm... it's not here" blocked screen.
    6. The same unhandled stale optimistic ID pattern exists in WorkArrangementPage.tsx:59 and WorkspaceMemberCustomFieldPage.tsx:46.

    What changes do you think we should make in order to solve the problem?

    1. Promote getMemberLoginByOptimisticAccountID to a shared helper:
      Extract getMemberLoginByOptimisticAccountID(policy, accountID) from WorkspaceMemberDetailsPage.tsx into src/libs/PolicyUtils.ts so all workspace member subpages can consume it consistently.

    2. Update src/pages/workspace/members/WorkspaceMemberDetailsRolePage.tsx:

      • Resolve memberLogin with fallback to getMemberLoginByOptimisticAccountID:
        const memberLogin = personalDetails?.[accountID]?.login ?? getMemberLoginByOptimisticAccountID(policy, accountID);
        const memberPersonalDetails = usePersonalDetailByLogin(memberLogin);
        const resolvedAccountID = memberPersonalDetails?.accountID ?? accountID;
      • Use resolvedAccountID in updateWorkspaceMembersRole(policy, [memberLogin], [resolvedAccountID], value), in isRuleBotEnforcingRules(resolvedAccountID, policy), and in the Navigation.goBack return route.
    3. Protect sibling member subpages:
      Apply the same getMemberLoginByOptimisticAccountID fallback in WorkArrangementPage.tsx:59 and WorkspaceMemberCustomFieldPage.tsx:46 to eliminate the same race condition when navigating directly to work arrangement or custom fields immediately after an invite.

    4. Automated UI Unit Testing:
      In tests/ui/WorkspaceMemberDetailsPageTest.tsx (or an equivalent WorkspaceMemberDetailsRolePageTest.tsx), add a test verifying that when WorkspaceMemberDetailsRolePage is rendered with a route holding a stale optimistic accountID (where personalDetails no longer contains that optimistic ID), the page resolves the employee role correctly and renders the role selection options without triggering the blocked screen.

    What alternative solutions did you explore? (Optional)

    1. Disabling interaction on the member row while pendingAction === ADD:
      While isInteractive: !details.isOptimisticPersonalDetail exists in parts of the members table, restricting user navigation is a workaround that hurts responsiveness and does not protect against deep links, back navigation, or tab restoration while an invite request is settling over slow networks. Recovering the login identity via getMemberLoginByOptimisticAccountID addresses the root cause directly across the entire lifecycle.

    Contributor details
    Your Expensify account email: [email protected]
    Upwork Profile Link: https://www.upwork.com/freelancers/~0170d6b02e9440e604

  9. melvin-bot commented on Oct 5, 2026

    @melvin-bot

    ✅ Contributor details stored successfully. Thank you for contributing to Expensify!

  10. franklincg commented on Oct 6, 2026

    @franklincg

    Proposal

    Root cause

    The Role page keeps the optimistic accountID in the route, but after AddMembersToWorkspace resolves that temporary personal-details entry is replaced. Its direct lookup then returns no login/member, so AccessOrNotFoundWrapper blocks the page. The parent Member Details page already handles this race via the #96449 optimistic-ID fallback.

    Fix

    Reuse that fallback in WorkspaceMemberDetailsRolePage, resolve the permanent accountID with usePersonalDetailByLogin, and use the resolved identity for permission checks, isRuleBotEnforcingRules, updateWorkspaceMembersRole, and back navigation.

    Test

    Keep the Role page mounted while Onyx swaps optimistic -> real accountID; assert it never shows "Hmm... it's not here" and the role update uses the permanent accountID.

    I would keep the production patch scoped to the Role flow unless a regression test proves another subpage needs the same change.

  11. melvin-bot commented on Oct 8, 2026

    @melvin-bot

    @marufsharifi Whoops! This issue is 2 days overdue. Let's get this updated quick!

  12. marufsharifi commented on Oct 9, 2026

    @marufsharifi
    Contributor

    checking this today.

  13. jloa-dev commented on Oct 11, 2026

    @jloa-dev

    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    When inviting a new (non-existing Expensify) member to a workspace, navigating into the member row and clicking on "Role" quickly triggers a "Not Found" error / blocking screen instead of displaying the role options.

    What is the root cause of that problem?

    When an admin invites a new user, NewDot assigns an optimistic accountID (generated via generateAccountID(login)).
    When the backend AddMembersToWorkspace call finishes:

    1. The backend provides the permanent accountID, removing the temporary personal details entry.
    2. In src/pages/workspace/members/WorkspaceMemberDetailsRolePage.tsx (line 40):
      const memberLogin = personalDetails?.[accountID]?.login ?? '';
      const member = policy?.employeeList?.[memberLogin];
      const canManageSelectedMemberRole = canMemberAssignRole(policy, currentUserLogin, member?.role);
      WorkspaceMemberDetailsRolePage looks up personalDetails?.[accountID]. Since accountID in route params is the optimistic ID, personalDetails?.[accountID] is undefined.
    3. memberLogin falls back to '', so member is undefined, and canMemberAssignRole evaluates to false.
    4. AccessOrNotFoundWrapper has shouldBeBlocked={!canManageSelectedMemberRole}, immediately blocking the screen with an error.

    While WorkspaceMemberDetailsPage.tsx already addressed this in PR #96449 using getMemberLoginByOptimisticAccountID, this helper was not exported nor used in WorkspaceMemberDetailsRolePage.tsx.

    What changes do you think we should make in order to solve the problem?

    1. Export getMemberLoginByOptimisticAccountID in src/libs/PolicyUtils/index.ts (or src/libs/PolicyMemberRoleUtils.ts):

      function getMemberLoginByOptimisticAccountID(policy: OnyxEntry<Policy>, accountID: number): string {
          const matchesOptimisticAccountID = (login: string) => generateAccountID(login) === accountID || generateAccountID(Str.removeSMSDomain(login)) === accountID;
          const primaryLoginOfInvitedSecondary = Object.entries(policy?.primaryLoginsInvited ?? {}).find(([secondaryLogin]) => matchesOptimisticAccountID(secondaryLogin))?.[1];
          if (primaryLoginOfInvitedSecondary) {
              return primaryLoginOfInvitedSecondary;
          }
          return Object.keys(policy?.employeeList ?? {}).find(matchesOptimisticAccountID) ?? '';
      }
    2. Update src/pages/workspace/members/WorkspaceMemberDetailsRolePage.tsx:

      • Recover member login and resolved accountID using getMemberLoginByOptimisticAccountID:
      const routeAccountID = Number(route.params.accountID);
      const memberLogin = personalDetails?.[routeAccountID]?.login ?? getMemberLoginByOptimisticAccountID(policy, routeAccountID);
      const member = policy?.employeeList?.[memberLogin];
      const resolvedAccountID = personalDetailByLogin?.[memberLogin]?.accountID ?? routeAccountID;
      • Use resolvedAccountID for role updates and back navigation:
      updateWorkspaceMembersRole(policy, [memberLogin], [resolvedAccountID], value);
      Navigation.goBack(ROUTES.WORKSPACE_MEMBER_DETAILS.getRoute(policyID, resolvedAccountID));
    3. In src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx:
      Import getMemberLoginByOptimisticAccountID from @libs/PolicyUtils instead of inlining it.

    What alternative solutions did you explore? (Optional)

    • Forcing a blocking modal until AddMembersToWorkspace finishes: Poor UX; optimism is a core principle in New Expensify. Recovering the member login preserves instant responsiveness.

    What specific scenarios should we cover in automated tests?

    In tests/ui/WorkspaceMemberDetailsRolePageTest.tsx:

    1. Navigating to WorkspaceMemberDetailsRolePage with an optimistic accountID successfully resolves the member from policy.employeeList and renders the role list.
    2. Changing role with an optimistic ID dispatches updateWorkspaceMembersRole with the resolved ID and navigates back cleanly without errors.
  14. marufsharifi commented on Oct 12, 2026

    @marufsharifi
    Contributor

    reproduced, i will check the proposal today.

  15. marufsharifi commented on Oct 13, 2026

    @marufsharifi
    Contributor

    @MelvinBot implement this

  16. MelvinBot commented on Oct 13, 2026

    @MelvinBot
    Contributor

    🤖 Draft PR created: Keep the workspace member Role page working right after inviting a member
    Please mark it as "Ready for review" or ask me to do that for you when it is ready for review.

    The PR implements the proposal. The Role, Work arrangement, and custom field pages now find the member even when the route still holds the optimistic accountID. A new Role page UI test covers this case.

    The automated web smoke test couldn't run because the test browser showed a blank page. Please test it manually before review.

  17. melvin-bot commented on Oct 16, 2026

    @melvin-bot

    @marufsharifi Uh oh! This issue is overdue by 2 days. Don't forget to update your issues!

  18. marufsharifi commented on Oct 17, 2026

    @marufsharifi
    Contributor

    PR still under review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

BugSomething is broken. Auto assigns a BugZero manager.ExternalAdded to denote the issue can be worked on by a contributorReviewingHas a PR in reviewWeeklyKSv2

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions