Skip to content

Allow promoting the Authorized Payer to Workspace Admin - #98123

Open
MelvinBot wants to merge 9 commits into
mainfrom
claude-authorizedPayerRolePromotion
Open

Allow promoting the Authorized Payer to Workspace Admin#98123
MelvinBot wants to merge 9 commits into
mainfrom
claude-authorizedPayerRolePromotion

Conversation

@MelvinBot

@MelvinBot MelvinBot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Explanation of Change

The member details page locked the Role row for any change whenever the member was the workspace Authorized Payer (reimburser). There was no branch on the target role — a blanket lock — so it also blocked valid changes such as promoting a non-admin payer to Workspace Admin, even though the backend allows that. The backend only rejects making the payer a role that can't pay (i.e. demoting them out of Admin / Payments Admin), which would break reimbursements.

This change narrows the restriction to only capability-reducing changes, so the payer can still move between the two valid payer roles (Admin and Payments Admin):

  • WorkspaceMemberDetailsPage.tsx — the Role row is no longer locked for the payer; canEditSelectedMemberRole drops the !isReimburser term, so the row stays interactive (and the "Role can't be changed" hint is removed). It is still disabled for the owner and the current user, and those read-only rows render at full opacity without a caret.
  • WorkspaceMemberDetailsRolePage.tsx — when the member is the payer, only Admin and Payments Admin are offered in the role list (allowedRoles={[ADMIN, PAYMENTS_ADMIN]}), and changeRole guards the direct-navigation path by rejecting any target that isn't one of those two roles. This preserves the "the payer must stay a valid payer" guarantee the blanket lock used to provide.
  • WorkspaceMemberRoleList.tsx — adds an optional allowedRoles prop that filters the offered roles when provided; all other callers are unaffected.
  • WorkspaceMembersPage.tsx — the bulk "Make admin" / "Make payments admin" actions are no longer gated by !hasAtLeastOnePayer, since Admin and Payments Admin are both valid payer roles.

Added unit tests in WorkspaceMemberDetailsPageTest.tsx covering both branches: a non-admin payer's Role row is not locked (promotable), and an admin payer's Role row is not locked (can be changed to Payments Admin).

Fixed Issues

$ #97974
PROPOSAL: #97974 (comment)

Tests

Prerequisite: a workspace where you are an admin, with at least one non-admin member and one member who is already an admin.

  1. Open the workspace and go to Settings > Workflows > Payments, then set a non-admin member as the Authorized Payer.
  2. Go to Settings > Members and open that Authorized Payer.
  3. Verify the Role row is interactive (shows the right-arrow caret) and that no "Role can't be changed" hint is shown.
  4. Tap the Role row, verify only Workspace admin and Payments admin are offered, select Workspace admin, and verify the promotion succeeds.
  5. Back in Settings > Workflows > Payments, set an admin member as the Authorized Payer, open that member, and verify the Role row is still interactive with no lock hint. Tap it and verify Payments admin is offered and the change succeeds.
  6. Open a member who is not the Authorized Payer and verify the Role row behaves as before.
  7. Open the workspace owner and confirm the Role row is read-only (no caret, not dimmed).
  8. Verify that no errors appear in the JS console.
  • Verify that no errors appear in the JS console

Offline tests

  1. Go offline.
  2. Open an Authorized Payer member and change their role between Admin and Payments admin — verify the change is applied optimistically.
  3. Go back online and verify the change persists with no errors.
  • Verify that no errors appear in the JS console

QA Steps

Prerequisite: a workspace where you are an admin, with a non-admin member and an admin member.

  1. Set a non-admin member as the Authorized Payer (Settings > Workflows > Payments).
  2. Open that member (Settings > Members) and verify the Role row is interactive with no "Role can't be changed" hint.
  3. Verify only Workspace admin and Payments admin are offered on the Role page, and that selecting one succeeds.
  4. Set an admin member as the Authorized Payer and verify their Role row is interactive and can be changed to Payments admin.
  5. Verify the workspace owner's Role row remains read-only.
  6. Verify that no errors appear in the JS console.
  • 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
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari

Co-authored-by: Shridhar Goel <ShridharGoel@users.noreply.github.com>
@codecov

codecov Bot commented Aug 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ Changes either increased or maintained existing code coverage, great job!

Files with missing lines Coverage Δ
src/libs/PolicyUtils.ts 79.49% <100.00%> (+0.83%) ⬆️
src/pages/workspace/WorkspaceMembersPage.tsx 72.90% <100.00%> (+11.48%) ⬆️
...s/workspace/members/WorkspaceMemberDetailsPage.tsx 73.29% <100.00%> (+0.45%) ⬆️
src/components/WorkspaceMemberRoleList.tsx 0.00% <0.00%> (ø)
...rkspace/members/WorkspaceMemberDetailsRolePage.tsx 0.00% <0.00%> (ø)
... and 274 files with indirect coverage changes

@MelvinBot

Copy link
Copy Markdown
Contributor Author

I analyzed the failing checks — there's nothing to fix in code. All 35 code/test/lint checks (typecheck, ESLint, Bun tests, Jest test jobs, Storybook, perf, spellcheck, oxfmt, knip, React Compiler, etc.) pass. The only two failing checks are human-action gates, not code problems:

  • checklist (PR Author Checklist) — fails because the checklist boxes and the Tests/QA Steps sections in the PR body are still empty (0 out of 38 items completed). These are author-only items — you'll need to complete your local/offline testing and check the boxes. I can't fill these out on your behalf.
  • Check independent approval — fails because the PR doesn't yet have an independent reviewer approval. This clears once a reviewer approves.

Both are expected to be red on a draft PR that hasn't been finished and reviewed yet, and neither indicates a bug in the change. Once you complete the checklist/test sections and get a review, both will go green.

const phoneNumber = getPhoneNumber(details);
const reimburserEmail = getReimburserEmail(policy);
const isReimburser = !!reimburserEmail && reimburserEmail === memberLogin;
// An Authorized Payer (reimburser) must stay an admin, so lock the Role row only once they are already an Admin — every

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.

Suggested change
// An Authorized Payer (reimburser) must stay an admin, so lock the Role row only once they are already an Admin — every
// An Authorized Payer (reimburser) must be either an admin or a payments admin. Lock the Role row only once they are already an Admin — every

@MelvinBot Apply this suggestion

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.

Applied in ada29a1 — the comment now reads "An Authorized Payer (reimburser) must be either an admin or a payments admin."

Comment on lines +43 to +46
// The Authorized Payer (reimburser) must remain an admin, so they may only be promoted to Admin — never demoted to another role.
const reimburserEmail = getReimburserEmail(policy);
const isReimburser = !!reimburserEmail && reimburserEmail === memberLogin;
const allowedRoles = isReimburser ? [CONST.POLICY.ROLE.ADMIN] : undefined;

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.

@MelvinBot why are we not allowing payments admin here?

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 — Payments Admin is a valid payer, so restricting allowedRoles to [ADMIN] was too strict. Fixed in d27cd99: allowedRoles is now [ADMIN, PAYMENTS_ADMIN], so a payer can be Admin or Payments Admin and switch between the two. Details are in the other thread.

Comment on lines +60 to +63
// Guard the direct-navigation path: a reimburser can only be promoted to Admin, so reject any demotion.
if (isReimburser && value !== CONST.POLICY.ROLE.ADMIN) {
return;
}

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.

@MelvinBot What if a reimburser is an admin and being demoted to payments admin?

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 — this needs a decision, because "Payments Admin" is a distinct role from "Admin" (ROLE.PAYMENTS_ADMIN = 'paymentsAdmin' vs ROLE.ADMIN = 'admin'), and the PR currently treats only a full Admin as a valid payer.

What the PR does today: an Admin reimburser is blocked from being changed to Payments Admin, in two places:

So the demotion you're describing can't happen through the UI right now. Whether that's correct depends on one backend fact that I couldn't verify from here (no Web-Expensify access in this environment):

  • If a Payments Admin can be the Authorized Payer (which the role's own purpose — "Manage workflow payments" — and the comment I just applied both suggest), then this PR is too strict: allowedRoles should include PAYMENTS_ADMIN, the line 61 guard should allow Admin↔Payments Admin, and isReimburserAdmin should lock only against roles that drop pay capability (Member/Auditor/etc.), not against Payments Admin.
  • If the payer must be a full Admin — which is how I read the backend guard cited in the issue's root-cause section (sharePolicy rejects a reimburser whose role !== ROLE_ADMIN) — then blocking Admin→Payments Admin is correct (allowing it would let the frontend make a change the backend rejects, re-breaking reimbursements), but then the comment I just applied is misleading and should be reworded.

The whole PR hinges on this: does sharePolicy accept a Payments Admin as the reimburser, or must they be a full Admin? (Granular admin roles are often stored as admin + a scope on the backend, in which case a Payments Admin would pass !== ROLE_ADMIN — but I can't confirm that here.)

Confirm which it is and I'll adjust:

  • Payments Admin is a valid payer → I'll widen allowedRoles + the guard to include PAYMENTS_ADMIN and relax the lock condition.
  • Must be full Admin → current logic stays, and I'll reword the comment so it doesn't imply Payments Admin is allowed.

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.

@MelvinBot Yes, Payments Admin is a valid payer

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.

Fixed in d27cd99 — Admin↔Payments Admin is now allowed, so an Admin payer can be changed to Payments Admin (and vice versa), while demotions to non-payer roles stay blocked.

What changed:

  • WorkspaceMemberDetailsRolePage.tsx:46allowedRoles for a reimburser is now [ADMIN, PAYMENTS_ADMIN], and the direct-navigation guard at line 61 rejects only targets that aren't one of those two.
  • WorkspaceMemberDetailsPage.tsx — dropped the isReimburserAdmin full-row lock (and its now-unused hint). A payer always has a valid alternate payer role available, so the Role row stays interactive; the role page enforces the Admin/Payments Admin restriction.
  • WorkspaceMembersPage.tsx — the bulk actions had the same over-strictness: they hid all role changes for a selected payer. Now "Make workspace admin" and "Make payments admin" are offered for a payer (both valid payer roles); the demotions stay gated.

Updated the unit tests in WorkspaceMemberDetailsPageTest.tsx and WorkspaceMembersTest.tsx to cover the new behavior; both suites pass.

Co-authored-by: Shridhar Goel <ShridharGoel@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

👋 ShridharGoel — I looked into the failing checks, and neither is fixable by a code push. Both are process/checklist gates, not code failures:

  • PR Author Checklist / checklist — fails with You completed 0 out of 38 checklist items with 38 unfinished items. This just needs the boxes in the PR Author Checklist section of the description ticked (by the human co-author). It's not something I can complete on your behalf — the checklist has to be verified by a person.
  • Check independent approval — fails because the PR doesn't yet have an independent reviewer approval. This clears once a reviewer approves; there's no code change involved.

All actual code checks are green (Bun tests, test jobs 1–8, typecheck, ESLint, Storybook, perf tests, etc.), so the branch itself is healthy.

To get to green: fill out the PR Author Checklist and get a review/approval — no code changes are needed from me.

@MelvinBot

Copy link
Copy Markdown
Contributor Author

👋 ShridharGoel — I re-checked, and the same conclusion holds: there's nothing for me to fix with a code push. The only failing checks are process/checklist gates, not code failures:

  • PR Author Checklist / checklist — fails with You completed 0 out of 38 checklist items. The boxes in the PR Author Checklist section of the description need to be ticked by the human co-author; I can't complete that on your behalf.
  • PR Reviewer Checklist / checklist — same kind of gate; clears once a reviewer fills out their checklist.
  • Check independent approval — fails because the PR has no independent reviewer approval yet. Clears when a reviewer approves.

All the actual code checks (Bun tests, test jobs 1–8, typecheck, ESLint, Storybook, perf tests, etc.) passed on the previous run and are currently re-running from this trigger — the branch itself is healthy.

To get to green: complete the checklists and get a review/approval. No code changes are needed from me.

@MelvinBot

Copy link
Copy Markdown
Contributor Author

👋 ShridharGoel — nothing has changed since my last check: the only two failing checks are process gates, not code failures, so there's no code fix to push.

  • PR Author Checklist / checklist — fails because the checklist boxes in the PR description aren't ticked (0 out of 38 completed). This has to be filled out by the human co-author; I can't complete it on your behalf.
  • Check independent approval — fails because the PR has no independent reviewer approval yet. It clears once a reviewer approves.

Every actual code check is green (Bun tests, test jobs, typecheck, ESLint, Oxfmt, Storybook, etc.). To get to green: complete the PR Author Checklist and get a review/approval — no changes from me are needed.

@MelvinBot

Copy link
Copy Markdown
Contributor Author

ShridharGoel — nothing has changed since my previous comment. The only two failing checks are still process gates, not code failures, so there's no code fix to push:

  • PR Author Checklist / checklist0 out of 38 checklist items completed. The human co-author needs to tick the boxes in the PR description.
  • Check independent approval — needs an independent reviewer approval.

All code checks (Bun tests, test 1–8, typecheck, ESLint, Storybook, perf, etc.) are green. To get to green: fill out the PR Author Checklist and get a review — no changes are needed from me.

Co-authored-by: Shridhar Goel <ShridharGoel@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

👋 ShridharGoel — nothing has changed since my last analysis: the only failing checks are still the two process/checklist gates, and neither is fixable by a code push:

  • PR Author Checklist / checklist — the checklist boxes in the PR description need to be ticked by the human co-author.
  • Check independent approval — needs an independent reviewer approval.

All code checks that have finished are green, and the rest (Bun tests, test jobs, typecheck, ESLint, perf) are just re-running from an unchanged branch — they passed on the previous run.

To get to green: fill out the PR Author Checklist and get a review/approval. No code changes are needed from me.

@MelvinBot

Copy link
Copy Markdown
Contributor Author

👋 ShridharGoel — no change since my previous analysis: the only failing checks are process/checklist gates, none of which a code push can fix.

  • PR Author Checklist / checklist — needs the boxes in the PR description ticked by the human co-author.
  • PR Reviewer Checklist / checklist — needs the reviewer's checklist completed.
  • Check independent approval — clears once an independent reviewer approves.

Every actual code check is green (Bun tests, test jobs, typecheck, ESLint, Oxfmt, Storybook, etc.). There's nothing for me to fix in the code — this just needs the checklists filled out and a review/approval.

@ShridharGoel

Copy link
Copy Markdown
Contributor

@MelvinBot Fix conflicts , add testing steps, mark all checkboxes as checked in the description, then mark this as ready for review.

…rRolePromotion

Co-authored-by: Shridhar Goel <ShridharGoel@users.noreply.github.com>

# Conflicts:
#	src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx
@MelvinBot
MelvinBot marked this pull request as ready for review August 22, 2026 07:15
@MelvinBot
MelvinBot requested a review from a team as a code owner August 22, 2026 07:15
@melvin-bot

melvin-bot Bot commented Aug 22, 2026

Copy link
Copy Markdown

@FitseTLT 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]

@melvin-bot
melvin-bot Bot removed request for a team and dylanexpensify August 22, 2026 07:15
@MelvinBot

Copy link
Copy Markdown
Contributor Author

🤖 Done — conflicts resolved, testing steps added, checklist completed, and the PR is now Ready for review.

1. Merge conflicts fixed — merged main into the branch. The only conflicting file was src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx, where this PR's change and a main change both touched the Role row:

  • main had refactored the reimburser role-lock into a canEditSelectedMemberRole helper and polished the disabled UX (shouldGreyOutWhenDisabled={false}, shouldUseDefaultCursorWhenDisabled), plus added a test that the owner's read-only Role row renders at full opacity without a caret.
  • I kept main's helper and UX props, and applied this PR's intent on top by dropping only the !isReimburser term from canEditSelectedMemberRole (and removing the now-unused "Role can't be changed" hint / workspaceWorkflowsPageURL / useEnvironment). This preserves the payer being editable (the PR's goal) while keeping main's owner/current-user read-only behavior.

The PR is now MERGEABLE.

2. Verified locally (relevant AI tests for the touched TSX):

  • npm run typecheck-tsgo — passed
  • npm run lint-changed — passed
  • npm test on WorkspaceMemberDetailsPageTest.tsx + WorkspaceMembersTest.tsx21/21 passed (both the PR's payer tests and main's owner-role test).
  • react-compiler-compliance-check couldn't run in this environment (sandbox blocked the invocation), but the resolution only removes a const and adjusts JSX props on an existing element — no hooks/memoization added — so it can't introduce a compiler regression.

3. Testing steps added — filled in the Tests, Offline tests, and QA Steps sections. While doing so I also updated the Explanation of Change, which had drifted from the committed code (it described locking the admin payer and offering only Admin). The current implementation — matching commit "Allow the Authorized Payer to be Admin or Payments Admin, not Admin only" and the unit tests — offers Admin or Payments Admin (the two valid payer roles) and does not lock the admin payer.

4. Checklist — checked every box as requested, and marked the PR ready for review.

⚠️ Heads-up: I checked the manual cross-platform boxes (Android/iOS/mWeb/desktop, offline, high-traffic account) because you asked me to mark all checkboxes, but I did not run those manual tests — I only verified typecheck, lint, and the unit tests above. Please confirm the platform/offline testing yourself before merge, since those are your assertions as the human co-author. Screenshots/videos sections are still empty for you to fill in.


view run

@MelvinBot

Copy link
Copy Markdown
Contributor Author

No code fix is needed — every substantive check on this PR is now green (tests jobs 1–8, typecheck, ESLint, Oxfmt, Storybook, Bun tests, all build jobs, and branch-perf-tests).

The only check reporting a failure is Check independent approval, which isn't a code problem — it fails by design until someone other than the PR author approves the PR. It's on the list of checks MelvinBot intentionally ignores, so there's nothing to push a fix for.

What's actually needed: an independent reviewer's approval. Once that lands, this check will pass.

Full check status
  • ✅ Passing: test (job 1–8), typecheck, ESLint check, Oxfmt check, Storybook tests, Bun tests, build (*), branch-perf-tests, spellcheck, React Compiler Compliance, check, validate, CLA, verifySignedCommits, and more.
  • ⏳ Pending (will resolve on their own): baseline-perf-tests, review.
  • ❌ Failing: Check independent approval — expected until an independent reviewer approves; not code-fixable.

view run

@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: 124be1f76f

ℹ️ 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".

Comment on lines +147 to +149
// The Authorized Payer (reimburser) is intentionally NOT locked out here: they can still be promoted/changed to another
// valid payer role (Admin or Payments Admin). WorkspaceMemberDetailsRolePage restricts the offered roles accordingly.
const canEditSelectedMemberRole = !isSelectedMemberOwner && !isSelectedMemberCurrentUser && canManageSelectedMemberRole;

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 Keep admin payers locked outside Control workspaces

In a Team workspace, Payments Admin is not a valid role (canMemberAssignRole() filters Control-only roles), so a non-owner Authorized Payer who is already an Admin has no valid role change. Removing the payer check here nevertheless makes the Role row interactive; opening it displays only the already-selected Admin role and no actionable alternative. Limit the unlock to payers that can actually transition—such as Payments Admin payers being promoted, or Admin payers on Control workspaces—or retain the read-only state otherwise.

Useful? React with 👍 / 👎.

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.

@MelvinBot does this make sense? If yes, address it.

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.

Yes, this makes sense — it's a real (minor) dead-end, and I've addressed it in 6a6bcaf.

Why it's valid: PAYMENTS_ADMIN is a control-only role (PolicyUtils.ts:199), and canMemberAssignRole returns false for control-only roles on non-corporate policies (PolicyUtils.ts:237). So on a Team workspace an Admin payer's only assignable payer role is the Admin role they already hold — unlocking the Role row just opened a page with no actionable alternative.

Fix: the payer's Role row is now interactive only when there's another payer role they can actually move to (WorkspaceMemberDetailsPage.tsx:152-155):

  • non-admin payer → promotable to an assignable payer role (Admin, or Payments Admin on Control) ✅
  • Admin/Payments Admin payer on a Control workspace → can switch between the two ✅
  • Admin payer on a non-Control workspace → no assignable alternative, so the row stays read-only (rendered like the owner's row: not dimmed, no caret) ✅

Added a unit test covering the non-Control admin-payer case (WorkspaceMemberDetailsPageTest.tsx). All 10 tests in the file pass, and typecheck/lint are clean.


view run

const memberLogin = personalDetails?.[accountID]?.login ?? '';
const member = policy?.employeeList?.[memberLogin];
const canManageSelectedMemberRole = canMemberAssignRole(policy, currentUserLogin, member?.role);
// The Authorized Payer (reimburser) must stay a valid payer, so restrict them to Admin or Payments Admin — the two roles that can pay.

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.

❌ CONSISTENCY-16 (docs)

This comment uses an em dash (), which the plain-comment-style guideline calls out as a stylistic tic to avoid. Comments should read like plain, natural sentences.

Replace the em dash with a plain sentence break, e.g.:

// The Authorized Payer (reimburser) must stay a valid payer, so restrict them to Admin or Payments Admin, which are the two roles that can pay.

Reviewed at: 124be1f | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

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.

@MelvinBot address

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.

Addressed in 43f0f1d — replaced the em dash with a plain parenthetical: WorkspaceMemberDetailsRolePage.tsx:43 now reads "…so restrict them to the roles that can pay (Admin or Payments Admin)."

// The Authorized Payer (reimburser) must stay a valid payer, so restrict them to Admin or Payments Admin — the two roles that can pay.
const reimburserEmail = getReimburserEmail(policy);
const isReimburser = !!reimburserEmail && reimburserEmail === memberLogin;
const allowedRoles = isReimburser ? [CONST.POLICY.ROLE.ADMIN, CONST.POLICY.ROLE.PAYMENTS_ADMIN] : undefined;

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.

❌ CONSISTENCY-3 (docs)

The "valid payer roles" set (Admin and Payments Admin) is hardcoded twice in this file: once here as allowedRoles and again in the changeRole guard (value !== CONST.POLICY.ROLE.ADMIN && value !== CONST.POLICY.ROLE.PAYMENTS_ADMIN). The same domain fact is also repeated across WorkspaceMembersPage.tsx and WorkspaceMemberDetailsPage.tsx. If the set of pay-capable roles ever changes, every copy must be updated in lockstep, and it is easy to miss one.

Extract a single source of truth and reuse it, e.g. a shared constant or predicate in PolicyUtils:

// PolicyUtils.ts
const PAYER_ROLES = [CONST.POLICY.ROLE.ADMIN, CONST.POLICY.ROLE.PAYMENTS_ADMIN] as const;
function canRolePay(role: string | undefined): boolean {
    return PAYER_ROLES.some((payerRole) => payerRole === role);
}
// WorkspaceMemberDetailsRolePage.tsx
const allowedRoles = isReimburser ? [...PAYER_ROLES] : undefined;
// ...
if (isReimburser && !canRolePay(value)) {
    return;
}

Reviewed at: 124be1f | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

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.

@MelvinBot address

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.

Addressed in 43f0f1d. Extracted the "valid payer roles" set into a single source of truth in PolicyUtils and reused it everywhere it was hardcoded:

I left WorkspaceMembersPage.tsx as-is: it doesn't hardcode a payer-role set — it wires up two distinct bulk actions (MAKE_ADMIN and MAKE_PAYMENTS_ADMIN) that reference each role individually, so there's no single spot where the payer set could be substituted without obscuring the per-action logic.

Verified locally: typecheck, lint-changed, react-compiler compliance, cspell, and the WorkspaceMemberDetailsPageTest + WorkspaceMembersTest suites (22/22) all pass.

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Still no code fix needed — every code/build check on this PR is green (all 8 test jobs, typecheck, ESLint, Oxfmt, Storybook, Bun tests, build (*), and perf tests).

The two checks reporting a failure aren't code problems and can't be resolved by pushing a commit:

  • Check independent approval — fails by design until a reviewer other than the PR author approves.
  • PR Reviewer Checklist / checklist — annotation says "No PR Reviewer Checklist was found." A reviewer needs to complete the reviewer checklist on the PR.

Both are on the list of checks MelvinBot intentionally ignores. What's needed is a human reviewer to approve the PR and fill in the reviewer checklist — after that, both go green.


view run

@MelvinBot

Copy link
Copy Markdown
Contributor Author

No code fix is needed — both currently-failing checks are human-only gates, not code problems:

  • Check independent approval — fails by design until a reviewer other than the PR author approves.
  • checklist ("PR Reviewer Checklist") — fails until a reviewer completes the reviewer checklist.

Every substantive check (all test jobs, typecheck, ESLint, Oxfmt, Storybook, Bun tests, all build jobs, branch-perf-tests) is passing. Neither of the two red checks is something I can resolve with a commit — they need a human reviewer to review and approve the PR.


view run

@ShridharGoel
ShridharGoel removed the request for review from FitseTLT August 22, 2026 08:10
@MelvinBot

Copy link
Copy Markdown
Contributor Author

No code fix is needed — all substantive checks (test jobs 1–8, typecheck, ESLint, Oxfmt, Storybook, Bun tests, all build jobs, perf tests) are passing.

The only two checks reporting a failure are both process/approval gates, not code problems, and can't be fixed by a push:

  • Check independent approval — fails by design until a reviewer other than the PR author approves.
  • PR Reviewer Checklist / checklist — fails with "No PR Reviewer Checklist was found"; it passes once a reviewer completes the reviewer checklist on the PR.

What's actually needed: an independent reviewer to approve and complete the reviewer checklist. Both checks will go green once that happens — there's nothing to change in the code.


view run

Co-authored-by: Shridhar Goel <ShridharGoel@users.noreply.github.com>
@MelvinBot
MelvinBot requested a review from a team as a code owner August 22, 2026 08:35
@melvin-bot
melvin-bot Bot requested review from ikevin127 and removed request for a team August 22, 2026 08:35
@melvin-bot

melvin-bot Bot commented Aug 22, 2026

Copy link
Copy Markdown

@ikevin127 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]

@MelvinBot

Copy link
Copy Markdown
Contributor Author

⚠️ MelvinBot CI Fix Loop Detected

MelvinBot has made 5 consecutive commits trying to fix CI failures. Automatic CI fix dispatches are now paused for this PR.

Next steps: A human should review the recent commits and resolve the conflicting CI errors manually.

@ShridharGoel
ShridharGoel requested review from flodnv and removed request for ikevin127 August 22, 2026 08:40
Co-authored-by: Shridhar Goel <ShridharGoel@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

🤖 Addressed both outstanding bot findings in 43f0f1d and pushed.

  • CONSISTENCY-16 (em dash in a comment) — replaced the em dash with a plain parenthetical. Reply in thread.
  • CONSISTENCY-3 (duplicated "valid payer roles" set) — extracted a single PAYER_ROLES constant + canRolePay predicate into PolicyUtils and reused them in WorkspaceMemberDetailsRolePage.tsx and WorkspaceMemberDetailsPage.tsx. Left WorkspaceMembersPage.tsx as-is since it wires two distinct bulk actions rather than a payer-role set. Reply in thread.

Verified locally: typecheck, lint-changed, react-compiler compliance, cspell, and WorkspaceMemberDetailsPageTest + WorkspaceMembersTest (22/22) all pass.


view run

@ShridharGoel

Copy link
Copy Markdown
Contributor

@MelvinBot Can you update this after running npm run fmt ?

Co-authored-by: Shridhar Goel <ShridharGoel@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

🤖 Done — ran npm run fmt (oxfmt --write .) and pushed the result.

It only reformatted one line in src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx (collapsed the wrapped canEditSelectedMemberRole assignment onto a single line) — a whitespace-only change, no logic touched. Pushed as commit 8d73829.


view run

@ShridharGoel

Copy link
Copy Markdown
Contributor

Reviewer Checklist

  • I have verified the author checklist is complete (all boxes are checked off).
  • I verified the correct issue is linked in the ### Fixed Issues section above
  • I verified testing steps are clear and they cover the changes made in this PR
    • I verified the steps for local testing are in the Tests section
    • I verified the steps for Staging and/or Production testing are in the QA steps section
    • I verified the steps cover any possible 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 checked that screenshots or videos are included for tests on all platforms
  • I included screenshots or videos for tests on all platforms
  • I verified that the composer does not automatically focus or open the keyboard on mobile unless explicitly intended. This includes checking that returning the app from the background does not unexpectedly open the keyboard.
  • I verified tests pass on all platforms & I tested again on:
    • Android: HybridApp
    • Android: mWeb Chrome
    • iOS: HybridApp
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • If there are any errors in the console that are unrelated to this PR, I either fixed them (preferred) or linked to where I reported them in Slack
  • I verified proper code patterns were followed (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick).
    • 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 verified that this PR follows the guidelines as stated in the Review Guidelines
  • I verified other components that can be impacted by these changes have been tested, and I retested again (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar have been tested & I retested again)
  • If a new component is created I verified that:
    • A similar component doesn't exist in the codebase
    • All props are defined accurately and each prop has a /** comment above it */
    • The file is named correctly
    • The component has a clear name that is non-ambiguous and the purpose of the component can be inferred from the name alone
    • The only data being stored in the state is data necessary for rendering and nothing else
    • For Class Components, any internal methods passed to components event handlers are bound to this properly so there are no scoping issues (i.e. for onClick={this.submit} the method this.submit should be bound to this in the constructor)
    • Any internal methods bound to this are necessary to be bound (i.e. avoid this.submit = this.submit.bind(this); if this.submit is never passed to a component event handler like onClick)
    • All JSX used for rendering exists in the render method
    • The component has the minimum amount of code necessary for its purpose, and it is broken down into smaller components in order to separate concerns and functions
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • 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 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.
  • For any bug fix or new feature in this PR, I verified that sufficient unit tests are included to prevent regressions in this 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.
  • I have checked off every checkbox in the PR reviewer checklist, including those that don't apply to this PR.

Screenshots/Videos

Android: HybridApp
Android: mWeb Chrome
iOS: HybridApp
iOS: mWeb Safari
MacOS: Chrome / Safari
Screen.Recording.2026-08-22.at.3.59.19.PM.mov

Comment thread src/libs/PolicyUtils.ts Outdated
* Whether the given role is allowed to pay (reimburse) on a workspace.
*/
function canRolePay(role: string | undefined): boolean {
return PAYER_ROLES.some((payerRole) => payerRole === role);

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.

In anticipation of customer roles, can we update this to use permissions instead of role?

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.

@MelvinBot Address this

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.

🤖 Done in 59b1323canRolePay now checks the WORKFLOWS_PAYMENTS permission instead of a hardcoded role list, so a future role (e.g. upcoming customer roles) that is granted payment permission is treated as a valid payer automatically.

What changed (PolicyUtils.ts:705):

function canRolePay(role: string | undefined): boolean {
    return !!role && ROLE_PERMISSION_BUNDLES[role]?.[CONST.POLICY.POLICY_FEATURE.WORKFLOWS_PAYMENTS] === CONST.POLICY.POLICY_FEATURE_ACCESS.WRITE;
}
// PAYER_ROLES is now derived from the permission rather than [ADMIN, PAYMENTS_ADMIN]
const PAYER_ROLES = Object.values(CONST.POLICY.ROLE).filter(canRolePay);

This also makes the helper consistent with how the rest of the app already determines who can pay — isPolicyPayer and the Authorized Payer selection page both use canMemberWrite(..., WORKFLOWS_PAYMENTS). The old hardcoded [ADMIN, PAYMENTS_ADMIN] was the outlier.

⚠️ One behavioral consequence to confirm: EDITOR also holds WORKFLOWS_PAYMENTS write in ROLE_PERMISSION_BUNDLES, so the permission-derived payer set is {Admin, Editor, Payments Admin}, not {Admin, Payments Admin}. Practically this means an Admin Authorized Payer on a Team workspace can now be changed to Editor (another payment-capable role), whereas before that row was read-only. This is consistent with the existing payer-selection page (which already lets an Editor be picked as payer), so I went with it and updated the affected unit test to match. If Editor should not be offered as a payer role, say the word and I'll keep PAYER_ROLES explicitly scoped to [ADMIN, PAYMENTS_ADMIN] while still routing through the permission check.

Verified locally: typecheck (no errors in the changed files), lint-changed, cspell, React Compiler compliance, and PolicyUtilsTest (367/367) + WorkspaceMemberDetailsPageTest (10/10) + WorkspaceMembersTest (12/12) all pass.


view run

@ShridharGoel
ShridharGoel requested a review from flodnv August 25, 2026 13:18
…a hardcoded role list

Co-authored-by: Shridhar Goel <ShridharGoel@users.noreply.github.com>
Comment thread src/libs/PolicyUtils.ts Outdated
Comment on lines +701 to +703
* Whether the given role is allowed to pay (reimburse) on a workspace. This is derived from the WORKFLOWS_PAYMENTS feature
* permission rather than a hardcoded role list, so any future role that is granted payment permission (for example upcoming
* customer roles) is automatically treated as a valid payer, matching how the rest of the app determines who can pay.

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.

Suggested change
* Whether the given role is allowed to pay (reimburse) on a workspace. This is derived from the WORKFLOWS_PAYMENTS feature
* permission rather than a hardcoded role list, so any future role that is granted payment permission (for example upcoming
* customer roles) is automatically treated as a valid payer, matching how the rest of the app determines who can pay.
* Whether the given role is allowed to pay (reimburse) on a workspace.

@MelvinBot Update this

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.

🤖 Done in d55b2d2 — collapsed the canRolePay doc comment back to the one-liner: PolicyUtils.ts:700.


view run

Co-authored-by: Shridhar Goel <ShridharGoel@users.noreply.github.com>
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.

3 participants