Skip to content

Keep workspace member details resolvable after an invite completes#96449

Open
wildan-m wants to merge 4 commits into
Expensify:mainfrom
wildan-m:wildan/95725-member-details-case-insensitive-lookup
Open

Keep workspace member details resolvable after an invite completes#96449
wildan-m wants to merge 4 commits into
Expensify:mainfrom
wildan-m:wildan/95725-member-details-case-insensitive-lookup

Conversation

@wildan-m

@wildan-m wildan-m commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Explanation of Change

Clicking a member right after inviting them opens the Right Hand Panel on "Hmm... it's not here" instead of the member's details. An invited member is initially listed under an optimistic account ID derived from their login, and once the invite request finishes the backend supplies their real account ID and the optimistic entry is discarded. A details page opened before that swap keeps holding the stale ID from its route, so its lookups come up empty and the page falls back to not-found — a deep link or refresh on such a URL fails the same way.

The page now recovers the member from what the stale route still encodes: it identifies the workspace employee whose login produces that optimistic ID (also allowing for the SMS domain that phone logins carry), resolves that login back to the member's current details, and continues with the real account ID, so the role and remove controls, card lookups, and navigation all operate on the member's actual account. A member whose route ID is already real resolves on the first lookup exactly as before, and someone who genuinely isn't in the workspace still gets the not-found page.

Fixed Issues

$ #95725
PROPOSAL: #95725 (comment)

Tests

  1. Go to Workspace settings > Members.
  2. Click Invite member, enter an email that has never been invited before (e.g. usera+fresh1@gmail.com), select it, click Next, then click Invite.
  3. Back on the Members page, immediately click the newly added member.
  4. Verify the Right Hand Panel shows the member's details (email and role) — and keeps showing them a few seconds later, once the invite request finishes.
  5. With the member details still open, reload the page on the same URL. Verify the member's details render instead of "Hmm... it's not here".
  6. Click another existing member. Verify their details open as before.
  7. Change the account ID at the end of the member details URL to a random number (e.g. .../members/999999). Verify the "Hmm... it's not here" page shows for a member that doesn't exist.
  • Verify that no errors appear in the JS console

Offline tests

  1. Enable offline mode (Settings > Troubleshoot > Force offline).
  2. From Workspace settings > Members, invite a brand-new email.
  3. Click the newly added member. Verify their details render while the invite is queued.
  4. Disable offline mode. Verify the open details panel keeps showing the member once the invite completes, with no "Hmm... it's not here" flash.

QA Steps

Same as tests.

  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
Kapture.2026-07-20.at.08.07.00.mp4
Android: mWeb Chrome
Kapture.2026-07-20.at.08.14.30.mp4
iOS: Native
Kapture.2026-07-20.at.08.00.11.mp4
iOS: mWeb Safari
Kapture.2026-07-20.at.08.02.30.mp4
MacOS: Chrome / Safari
Kapture.2026-07-19.at.21.13.01.mp4

wildan-m added 3 commits July 10, 2026 09:17
A freshly invited member is listed under an optimistic accountID derived
from their login. When the invite request finishes, that optimistic
personal-details entry is removed and the backend supplies the real
accountID, so a member details page opened during the invite was left
holding a stale accountID in its route and rendered the not-found page.

Recover the member's login from the employee list when the route
accountID no longer resolves, and use the real accountID from the
resulting personal details for the rest of the page.
@wildan-m
wildan-m marked this pull request as ready for review July 20, 2026 02:32
@wildan-m
wildan-m requested review from a team as code owners July 20, 2026 02:32
@melvin-bot
melvin-bot Bot requested review from JmillsExpensify and daledah and removed request for a team July 20, 2026 02:32
@melvin-bot

melvin-bot Bot commented Jul 20, 2026

Copy link
Copy Markdown

@daledah Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 57c91c185c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

const memberLogin = personalDetails?.[accountID]?.login ?? '';
const routeAccountID = Number(route.params.accountID);
const memberLogin = personalDetails?.[routeAccountID]?.login ?? getMemberLoginByOptimisticAccountID(policy, routeAccountID);
const memberPersonalDetails = personalDetails?.[routeAccountID] ?? getPersonalDetailByEmail(memberLogin);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resolve invited secondary logins before rendering details

When the stale route was created from a secondary login that the backend later adds under the account's primary login, getMemberLoginByOptimisticAccountID() can still return the secondary employee-list key, but getPersonalDetailByEmail(memberLogin) is undefined because personal details are stored under the primary login; this duplicate secondary/primary case is explicitly tracked by policy.primaryLoginsInvited. Since the not-found check only tests member, the RHP renders with empty details and keeps the optimistic accountID, so profile/role/remove actions can target the wrong login/account instead of the real member. Please map the secondary login to the primary login before resolving details, or require resolved personal details before rendering controls.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — handled in a745907. The recovery now consults the workspace's record of secondary-to-primary invites first, so a route ID derived from an invited secondary login resolves to the primary member's details instead of rendering with empty details or falling back to not-found once the secondary key is gone. Added a test case covering this scenario.

@JmillsExpensify JmillsExpensify left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants