Skip to content

Show a pending unlock request state for locked domain members - #104188

Open
MelvinBot wants to merge 11 commits into
mainfrom
claude-pendingUnlockRequestState
Open

MelvinBot wants to merge 11 commits into
mainfrom
claude-pendingUnlockRequestState

Conversation

@MelvinBot

@MelvinBot MelvinBot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Explanation of Change

This PR improves the domain member unlock request flow by providing clear feedback after an unlock request is submitted.

  • Updates the Unlock account option to Pending unlock request with an hourglass icon after a successful request.
  • Prevents duplicate unlock requests while a request is pending.
  • Reopens the We've received your request confirmation modal when the pending row is clicked, without sending another request.
  • Stores the pending unlock request state locally so it remains visible after refreshing the page.
  • Restores the Unlock account option and displays an error if the request fails.
  • Clears the previous pending unlock request state when the member's account is locked again.
  • Adds translations for the new label across supported languages.

Fixed Issues

$ #102734
PROPOSAL: #102734 (comment)

Tests

1: Open Domains → Domain Members.

2: Select a locked member.

3: Click Unlock account.

4: Verify the confirmation modal appears.

5: Click Got it.

6: Verify Pending unlock request and the hourglass icon appear.

7: Click the pending row and verify the modal reopens without sending another request.

  • Verify that no errors appear in the JS console

Offline tests

1: Open a locked domain member.

2: Go offline and click Unlock account.

3: Verify the request is queued.

4: Reconnect and verify the pending state updates correctly.

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 verified any copy / text that was added to the app 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 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 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.
  • If the PR adds or modifies the UI:
    • I asked an AI agent to review the changes for accessibility issues and addressed its findings.
    • I tested with a screen reader (VoiceOver on macOS) and verified all new/changed elements are reachable with a logical focus order.
    • I verified all new/changed elements have meaningful accessible names and roles.
    • I verified state changes are announced (e.g. checked/unchecked, expanded/collapsed, selected).
  • 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

After a domain admin requests an unlock for a locked member, the row now reads
"Pending unlock request" with an hourglass icon. Tapping it again reopens the
confirmation modal without sending a duplicate request, and a failed request
rolls back the state and shows an error on the row.

Co-authored-by: Maruf Sharifi <marufsharifi@users.noreply.github.com>
@MelvinBot
MelvinBot requested a review from a team October 16, 2026 06:55
@melvin-bot melvin-bot Bot added Melvin-Test-Android Melvin-Test-Web Triggers Melvin to run the testing steps of the PR on web labels Oct 16, 2026
@MelvinBot

Copy link
Copy Markdown
Contributor Author

I created this PR from an upstream branch since I don't have push access to your fork.

To take ownership of this branch and be able to push updates, run:

git remote add upstream https://github.com/Expensify/App.git
git fetch upstream claude-pendingUnlockRequestState
git checkout -b claude-pendingUnlockRequestState upstream/claude-pendingUnlockRequestState
git push -u origin claude-pendingUnlockRequestState

Then you can close this PR and open a new one from your fork.

@OSBotify

Copy link
Copy Markdown
Contributor

🦜 Polyglot Parrot! 🦜

Squawk! Looks like you added some shiny new English strings. Allow me to parrot them back to you in other tongues:

View the translation diff
diff --git a/src/languages/el.ts b/src/languages/el.ts
index 247aab54..15c21e53 100644
--- a/src/languages/el.ts
+++ b/src/languages/el.ts
@@ -2588,7 +2588,7 @@ const translations: TranslationDeepObject<typeof en> = {
         lockAccount: 'Κλείδωμα λογαριασμού',
         lockMyAccount: 'Κλείδωμα του λογαριασμού μου',
         unlockAccount: 'Ξεκλειδώστε τον λογαριασμό',
-        pendingUnlockRequest: 'Εκκρεμές αίτημα ξεκλειδώματος',
+        pendingUnlockRequest: 'Εκκρεμεί αίτημα ξεκλειδώματος',
         unlockTitle: 'Λάβαμε το αίτημά σας',
         unlockDescription: 'Θα εξετάσουμε τον λογαριασμό για να επιβεβαιώσουμε ότι είναι ασφαλές να ξεκλειδωθεί και θα επικοινωνήσουμε μέσω Concierge για τυχόν ερωτήσεις.',
         findYourSituation: 'Τα περισσότερα προβλήματα δεν απαιτούν κλείδωμα του λογαριασμού σας. Βρείτε την περίπτωσή σας παρακάτω:',
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index c0d23b8a..dc449409 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -2528,7 +2528,7 @@ const translations: TranslationDeepObject<typeof en> = {
         lockAccount: 'Account vergrendelen',
         lockMyAccount: 'Account vergrendelen',
         unlockAccount: 'Account ontgrendelen',
-        pendingUnlockRequest: 'Ontgrendelingsverzoek in behandeling',
+        pendingUnlockRequest: 'In behandeling zijnde ontgrendelingsverzoek',
         findYourSituation: 'Voor de meeste problemen hoef je je account niet te blokkeren. Zoek hieronder jouw situatie:',
         lostCardOrCharges:
             '<a href="https://help.expensify.com/articles/expensify-classic/expensify-card/Dispute-Transaction">Kaart kwijt of onbekende kosten</a>: Annuleer je kaart en neem contact op met Concierge om onbekende transacties te betwisten.',
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index 5cf2fb65..ffbb5941 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -2567,7 +2567,7 @@ const translations: TranslationDeepObject<typeof en> = {
         lockAccount: 'Zablokuj konto',
         lockMyAccount: 'Zablokuj moje konto',
         unlockAccount: 'Odblokuj konto',
-        pendingUnlockRequest: 'Oczekująca prośba o odblokowanie',
+        pendingUnlockRequest: 'Oczekujące żądanie odblokowania',
         findYourSituation: 'Większość problemów nie wymaga blokowania konta! Znajdź swoją sytuację poniżej:',
         lostCardOrCharges:
             '<a href="https://help.expensify.com/articles/expensify-classic/expensify-card/Dispute-Transaction">Zgubiona karta lub nieznane obciążenia</a>: Anuluj swoją kartę i skontaktuj się z Concierge, żeby zakwestionować nieznane transakcje.',
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index e30139d2..6e6479b5 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -2445,7 +2445,7 @@ const translations: TranslationDeepObject<typeof en> = {
         lockAccount: '锁定账户',
         lockMyAccount: '锁定我的账户',
         unlockAccount: '解锁账户',
-        pendingUnlockRequest: '解锁请求待处理',
+        pendingUnlockRequest: '待处理解锁请求',
         findYourSituation: '大多数问题都不需要锁定你的账户!请在以下列表中找到与你相符的情况:',
         lostCardOrCharges:
             '<a href="https://help.expensify.com/articles/expensify-classic/expensify-card/Dispute-Transaction">卡片遗失或出现不明扣款</a>:请挂失您的卡片,并联系 Concierge 申诉未知交易。',

Note

You can apply these changes to your branch by copying the patch to your clipboard, then running pbpaste | git apply 😉

View workflow run

@codecov

codecov Bot commented Oct 16, 2026 •

Copy link
Copy Markdown

Codecov Report

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

Files with missing lines Coverage Δ
src/CONST/index.ts 95.90% <ø> (ø)
src/libs/DomainUtils.ts 95.58% <100.00%> (+0.42%) ⬆️
src/libs/actions/Domain.ts 70.18% <ø> (ø)
src/selectors/Domain.ts 97.45% <100.00%> (+0.02%) ⬆️
...c/pages/domain/Members/DomainMemberDetailsPage.tsx 83.33% <92.30%> (+0.98%) ⬆️
src/libs/actions/User.ts 48.98% <84.61%> (+0.75%) ⬆️
... and 6 files with indirect coverage changes

@MelvinBot

MelvinBot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor Author

🤖 Android test steps verification results: ❌ fail

The Android test was blocked before step 1, so this result doesn't show a code defect. The test account is new, with no domain or workspace. That means it isn't a domain admin and has no member to lock and unlock. This PR's Tests section is still a TODO, so I took the steps from the linked issue's reproduction steps and the expected result in the PR description.
Domains tab empty state with no existing domain

Step Status
Precondition: signed in as a domain admin with a domain that has a non-admin memberThe Workspaces tab shows "No workspaces yet". The Domains tab shows the "Enhance your security with domains" empty state with only a "New domain" button. No domain or member list exists.
Domains tab empty state with no existing domain
❌
Open Workspaces > Domains > [domain] > Members and tap a member rowNot run. This account has no domain to open.
❌
Tap "Report suspicious activity"; the lock warning appearsNot run. No member row was available.
❌
Confirm the lock; the row shows "Unlock account"Not run. No member row was available.
❌
Tap "Unlock account", see "We've received your request", then tap "Got it"Not run. No member row was available.
❌
The row shows "Pending unlock request" with an hourglass iconNot run. No member row was available.
❌
Tap "Pending unlock request" again; the info modal reopens and no new request is sentNot run. No member row was available.
❌

To test this PR, you need a domain-admin account whose domain already has a non-admin member. Once Tests steps are added to the PR and such an account is available, you can run me again.


view run · view recording

@MelvinBot

MelvinBot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor Author

🤖 Web test steps verification results: ❌ fail

The Web test was blocked before step 1, so this result doesn't show a code defect. The test account has no domain, so it isn't a domain admin and has no member to lock and unlock. This PR's Tests section is still a TODO, so I took the steps from the linked issue's reproduction steps and the expected result in the PR description.
Domains tab showing no existing domain, only New domain button

Step Status
Precondition: signed in as a domain admin with a domain that has a non-admin memberThe Workspaces > Domains tab shows the "Enhance your security with domains" empty state with only a "New domain" button. No domain or member list exists.
Domains tab showing no existing domain, only New domain button
❌
Open Workspaces > Domains > [domain] > Members and click a member rowNot run. This account has no domain to open.
❌
Click "Report suspicious activity"; the lock warning appearsNot run. No member row was available.
❌
Confirm the lock; the row shows "Unlock account"Not run. No member row was available.
❌
Click "Unlock account", see "We've received your request", then click "Got it"Not run. No member row was available.
❌
The row shows "Pending unlock request" with an hourglass iconNot run. No member row was available. This is the main change in the PR.
❌
Click "Pending unlock request" again; the info modal reopens and no new request is sentNot run. No member row was available.
❌

To test this PR, you need a domain-admin account whose domain already has a non-admin member.


view run · view recording

@marufsharifi

Copy link
Copy Markdown
Contributor

@MelvinBot, replace the PR Author checklist with the verified one below:

PR author checklist

<!-- If necessary, assign reviewers that know the area or changes well. Feel free to tag any additional reviewers you see fit. -->

### Explanation of Change
<!-- Explain what your change does and how it addresses the linked issue -->

This PR improves the domain member unlock request flow by providing clear feedback after an unlock request is submitted.

- Updates the **Unlock account** option to **Pending unlock request** with an hourglass icon after a successful request.
- Prevents duplicate unlock requests while a request is pending.
- Reopens the **We've received your request** confirmation modal when the pending row is clicked, without sending another request.
- Stores the pending unlock request state locally so it remains visible after refreshing the page.
- Restores the **Unlock account** option and displays an error if the request fails.
- Clears the previous pending unlock request state when the member's account is locked again.
- Adds translations for the new label across supported languages.

### Fixed Issues
<!---
1. Please postfix `$` with a URL link to the GitHub issue this Pull Request is fixing. For example, `$ https://github.com/Expensify/App/issues/<issueID>`.
2. Please postfix  `PROPOSAL:` with a URL link to your GitHub comment, which contains the approved proposal (i.e. the proposal that was approved by Expensify).  For example, `PROPOSAL: https://github.com/Expensify/App/issues/<issueID>#issuecomment-1369752925`

Do NOT add the special GH keywords like `fixed` etc, we have our own process of managing the flow.
It MUST be an entire link to the github issue and your comment proposal ; otherwise, the linking and its automation will not work as expected.

Make sure this section looks similar to this (you can link multiple issues using the same formatting, just add a new line):

$ https://github.com/Expensify/App/issues/<issueID>
$ https://github.com/Expensify/App/issues/<issueID(comment)>

Do NOT only link the issue number like this: $ #<issueID>
--->
$ https://github.com/Expensify/App/issues/102734
PROPOSAL: https://github.com/Expensify/App/issues/102734#issuecomment-6196860603


<!--- 
If you want to trigger adhoc build of hybrid app from specific Mobile-Expensify PR please link it like this:

MOBILE-EXPENSIFY: https://github.com/Expensify/Mobile-Expensify/pull/<PR-number>

--->

### Tests
<!---
Add a numbered list of manual tests you performed that validates your changes work on all platforms, and that there are no regressions present.
Add any additional test steps if test steps are unique to a particular platform.
Manual test steps should be written so that your reviewer can repeat and verify one or more expected outcomes in the development environment.

For example:
1. Click on the text input to bring it into focus
2. Upload an image via copy paste
3. Verify a modal appears displaying a preview of that image
--->
1: Open Domains → Domain Members.

2: Select a locked member.

3: Click Unlock account.

4: Verify the confirmation modal appears.

5: Click Got it.

6: Verify Pending unlock request and the hourglass icon appear.

7: Click the pending row and verify the modal reopens without sending another request.

- [x] Verify that no errors appear in the JS console

### Offline tests
<!---
Add any relevant steps that validate your changes work as expected in a variety of network states e.g. "offline", "spotty connection", "slow internet", etc. Manual test steps should be written so that your reviewer and QA testers can repeat and verify one or more expected outcomes. If you are unsure how the behavior should work ask for advice in the `#expensify-open-source` Slack channel.
--->
1: Open a locked domain member.

2: Go offline and click Unlock account.

3: Verify the request is queued.

4: Reconnect and verify the pending state updates correctly.
### QA Steps
<!---
Add a numbered list of manual tests that can be performed by our QA engineers on the staging environment to validate that your changes work on all platforms, and that there are no regressions present.
Add any additional QA steps if test steps are unique to a particular platform.
Manual test steps should be written so that the QA engineer can repeat and verify one or more expected outcomes in the staging environment.

For example:
1. Click on the text input to bring it into focus
2. Upload an image via copy paste
3. Verify a modal appears displaying a preview of that image

It's acceptable to write "Same as tests" if the QA team is able to run the tests in the above "Tests" section.
--->
Same as Tests.

- [x] Verify that no errors appear in the JS console

### PR Author Checklist
<!--
This is a checklist for PR authors. Please make sure to complete all tasks and check them off once you do, or else your PR will not be merged!
-->

- [x] I linked the correct issue in the `### Fixed Issues` section above
- [x] I wrote clear testing steps that cover the changes made in this PR
    - [x] I added steps for local testing in the `Tests` section
    - [x] I added steps for the expected offline behavior in the `Offline steps` section
    - [x] I added steps for Staging and/or Production testing in the `QA steps` section
    - [x] I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    - [x] 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)
    - [x] I tested this PR with a [High Traffic account](https://github.com/Expensify/App/blob/main/contributingGuides/CONTRIBUTING.md#high-traffic-accounts) against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
- [x] I included screenshots or videos for tests on [all platforms](https://github.com/Expensify/App/blob/main/contributingGuides/CONTRIBUTING.md#make-sure-you-can-test-on-all-platforms)
- [x] I ran the tests on **all platforms** & verified they passed on:
    - [x] Android: Native
    - [x] Android: mWeb Chrome
    - [x] iOS: Native
    - [x] iOS: mWeb Safari
    - [x] MacOS: Chrome / Safari
- [x] 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)
- [x] I verified any copy / text that was added to the app 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)
- [x] If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
- [x] 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)
- [x] If new assets were added or existing ones were modified, I verified that:
    - [x] The assets are optimized and compressed (for SVG files, run `npm run compress-svg`)
    - [x] The assets load correctly across all supported platforms.
- [x] 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.
- [x] 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)
- [x] 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.
- [x] 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.
- [x] 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:
    - [x] I verified that all the inputs inside a form are aligned with each other.
    - [x] I added `Design` label and/or tagged `@Expensify/design` so the design team can review the changes.
- [x] If the PR adds or modifies the UI:
    - [x] I asked an AI agent to review the changes for accessibility issues and addressed its findings.
    - [x] I tested with a screen reader (VoiceOver on macOS) and verified all new/changed elements are reachable with a logical focus order.
    - [x] I verified all new/changed elements have meaningful accessible names and roles.
    - [x] I verified state changes are announced (e.g. checked/unchecked, expanded/collapsed, selected).
- [x] I added [unit tests](https://github.com/Expensify/App/blob/main/tests/README.md) for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
- [x] 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
<details>
<summary>Android: Native</summary>

<!-- add screenshots or videos here -->

</details>

<details>
<summary>Android: mWeb Chrome</summary>

<!-- add screenshots or videos here -->

</details>

<details>
<summary>iOS: Native</summary>

<!-- add screenshots or videos here -->

</details>

<details>
<summary>iOS: mWeb Safari</summary>

<!-- add screenshots or videos here -->

</details>

<details>
<summary>MacOS: Chrome / Safari</summary>

<!-- add screenshots or videos here -->

</details>




@MelvinBot

Copy link
Copy Markdown
Contributor Author

I replaced the PR description with your verified version, word for word.

Two things to check:

  • The Screenshots/Videos sections are still empty, but the checklist says screenshots are included for all platforms. Please add them before review.
  • Your version leaves out the earlier backend note. For the pending state to show on other devices or after a cache clear, the backend must set and return the private_unlockRequested_<accountID> domain key. An internal engineer still needs to confirm this.

view run

@marufsharifi

Copy link
Copy Markdown
Contributor

@MelvinBot Mark this as ready for review

@MelvinBot
MelvinBot marked this pull request as ready for review October 17, 2026 08:16
@MelvinBot
MelvinBot requested a review from a team as a code owner October 17, 2026 08:17
@MelvinBot

Copy link
Copy Markdown
Contributor Author

I marked this PR as ready for review.


view run

@melvin-bot
melvin-bot Bot requested review from trjExpensify and removed request for a team and trjExpensify October 17, 2026 08:17
@melvin-bot

melvin-bot Bot commented Oct 17, 2026

Copy link
Copy Markdown

@dubielzyk-expensify @marufsharifi One of you needs to 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 commented Oct 17, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-17T08:21:15.167936Z c76d719 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@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: c76d7190f5

ℹ️ 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 +140 to +142
if (!isUnlockRequested) {
requestUnlockAccount(accountID, domainAccountID, domainName);
}

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 Clear stale pending state before suppressing a new unlock request

When an earlier unlock completes and the account is later re-locked through another device, another administrator, or a server update, the locally persisted private_unlockRequested_* flag remains true because the only reset is inside this client's lockAccount() action. In that state this guard prevents RequestUnlockAccount from being sent, so clicking the newly locked member's row only reopens the old confirmation modal and the member cannot be unlocked from this client. Clear or reconcile the flag when the observed lock lifecycle changes rather than treating the local value as permanently authoritative.

Useful? React with 👍 / 👎.

@dubielzyk-expensify

Copy link
Copy Markdown
Contributor

What videos or screenshots show the change?

@marufsharifi

marufsharifi commented Oct 19, 2026 •

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 any copy / text that was added to the app 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 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
    • 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
    • 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 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.
  • If the PR adds or modifies the UI:
    • I asked an AI agent to review the changes for accessibility issues and addressed its findings.
    • I tested with a screen reader (VoiceOver on macOS) and verified all new/changed elements are reachable with a logical focus order.
    • I verified all new/changed elements have meaningful accessible names and roles.
    • I verified state changes are announced (e.g. checked/unchecked, expanded/collapsed, selected).
  • 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
Screen.Recording.2026-10-19.at.2.29.05.PM.mov
Android: mWeb Chrome
Screen_Recording_20261019_143233_Chrome.mp4
iOS: HybridApp
Screen.Recording.2026-10-19.at.2.43.24.PM.mov
iOS: mWeb Safari
Screen.Recording.2026-10-19.at.2.46.32.PM.mov
MacOS: Chrome / Safari
Screen.Recording.2026-10-19.at.1.53.39.PM.mov

@marufsharifi

Copy link
Copy Markdown
Contributor

Steps to reproduce:

  1. Sign in as a domain administrator.
  2. Navigate to Domains → Members.
  3. Open an already locked member.
  4. Open Chrome DevTools → Network and select Offline.
  5. Click Unlock account.
  6. Click Got it.
  7. Check the unlock action while still offline.

Expected Result: The app should not indicate that the server received the unlock request while it is still queued offline.

Actual Result: The action changes to Pending unlock request, even though the request has not reached the server.

RF-1.mov

@marufsharifi

Copy link
Copy Markdown
Contributor

@MelvinBot find the root cause of this: #104188 (comment)

@marufsharifi

Copy link
Copy Markdown
Contributor

@MelvinBot find the RCA for this bug: #104188 (comment)

@MelvinBot

Copy link
Copy Markdown
Contributor Author

RCA: failed unlock request doesn't show the red dot on Domain members

Root cause: The sidebar and Domains list use a different error check from the member row, and that check doesn't know about unlockAccountErrors.

  • When the request fails, the failure data stores the error in memberErrors[accountID].unlockAccountErrors (User.ts:1628).
  • The member row and the member details page read that field, so they show the error (DomainUtils.ts:166, DomainMemberDetailsPage.tsx:223).
  • The Domain members row uses hasDomainMembersErrors (getDomainMenuItems.ts:66). The Domains list uses hasDomainErrors, which calls the same function (DomainUtils.ts:52).
  • hasDomainMembersErrors checks errors plus the fields in hasDomainMemberDetailsErrors. That list has only vacationDelegateErrors, twoFactorAuthExemptEmailsError, and changeDomainSecurityGroupErrors (DomainUtils.ts:136-142).

This PR added unlockAccountErrors to the row's error merge but not to hasDomainMemberDetailsErrors. So the member shows an error, but neither dot appears.

Fix: Add unlockAccountErrors to hasDomainMemberDetailsErrors. lockAccountErrors has the same gap on main, so add it too. Then a failed lock or unlock shows the red dot on both the Domain members row and the Domains list. A unit test for hasDomainMembersErrors with only unlockAccountErrors set would cover this.

Confidence

High. I traced each surface to the function it reads. I did not reproduce this in a browser.


Reply with @MelvinBot implement this to push the fix to this PR.

@marufsharifi

Copy link
Copy Markdown
Contributor

@MelvinBot fix this: #104188 (comment)

Co-authored-by: Maruf Sharifi <marufsharifi@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

I fixed this in 52aa62a. A failed unlock request now shows the red dot on the Domain members row and on the Domains list.

hasDomainMemberDetailsErrors now checks unlockAccountErrors and lockAccountErrors (DomainUtils.ts:136-144). The lock check closes the same gap that already exists on main.

I added two tests: one with only an unlock error, and one with only a lock error (DomainUtilsTest.ts:361-381).

Checks
  • DomainUtilsTest.ts and DomainTest.ts: 137/137 pass.
  • npm run typecheck: pass.
  • npm run lint on the changed files: pass.
  • I did not test this in a browser.

Co-authored-by: Maruf Sharifi <marufsharifi@users.noreply.github.com>
@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.

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Fixed the failing Oxfmt check. It flagged two missing spaces after : in tests/unit/DomainUtilsTest.ts. I pushed the formatter output in 364c27d6, and that check now passes.

One other check still fails, but not because of this PR:

  • test (job 7): The Jest worker was killed with SIGKILL while running tests/unit/components/FormHelpMessageTest.tsx. This PR doesn't touch that file. All 3,588 tests that ran passed. This looks like a runner memory flake. Please rerun the failed job.

ESLint check was still running after 25 minutes. Local ESLint on the PR's main files is clean.

Check independent approval will pass once a reviewer approves.


view run

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Both failing checks are unrelated to this PR, so I didn't push a fix.

  • ESLint check: main has an unused eslint-disable directive at src/libs/BankAccountUtils.ts:146. This PR doesn't touch that file.
  • test (job 7): The Jest worker running tests/unit/components/FormHelpMessageTest.tsx was killed by SIGKILL, likely from running out of memory. That's a CI infrastructure failure, not a test failure: all 3,588 tests passed.

To get green, someone needs to delete that directive on main. Then merge main into this branch and rerun the failed Jest job.

Details
  • [Payment due @ZhenjaHorbach] [NoQA][Wise][Release 1] Business - UI - Entry Point Workspace added the directive to main today. Main's ESLint config sets ignorePrimitives: true for @typescript-eslint/prefer-nullish-coalescing. Because bankCurrency is a string, the rule never fires there, so ESLint reports the directive as unused.
  • CI lints this PR merged with the latest main. This branch is 1,771 commits behind main and changes src/CONST/index.ts, which BankAccountUtils.ts imports. That change likely forces the file to be linted fresh instead of reusing the cached result. That would explain why other PRs still pass.
  • ESLint and Jest both passed on this PR's previous commit, bc73674f.

view run

@marufsharifi

Copy link
Copy Markdown
Contributor

@MelvinBot merge the main.

@MelvinBot

Copy link
Copy Markdown
Contributor Author

I merged main into this branch and pushed it. The merge brought in 2,015 commits and had no conflicts. Merge commit: 7ca4482.


view run

@marufsharifi

Copy link
Copy Markdown
Contributor

@MelvinBot Merge the main.

…equestState

Co-authored-by: Maruf Sharifi <marufsharifi@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

I merged the latest main into this branch and pushed it (5c9231a). There were no conflicts.


view run

@marufsharifi

Copy link
Copy Markdown
Contributor

@MelvinBot Resolve the conflicts.

…equestState

Co-authored-by: Maruf Sharifi <marufsharifi@users.noreply.github.com>

# Conflicts:
#	src/pages/domain/Members/DomainMemberDetailsPage.tsx
@MelvinBot

Copy link
Copy Markdown
Contributor Author

I resolved the merge conflicts with main and pushed the merge commit (de5c5d7).

The only conflict was in the imports of src/pages/domain/Members/DomainMemberDetailsPage.tsx:

  • I kept the PR's unlockRequestedSelector import.
  • I took main's removal of useCallback, because nothing in the file uses it anymore.

All other files, including the language files, merged automatically. I couldn't run the repo's lint wrapper or typecheck in this environment, so please let CI confirm.


view run

@marufsharifi

Copy link
Copy Markdown
Contributor

@MelvinBot Merge the main.

…equestState

Co-authored-by: Maruf Sharifi <marufsharifi@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Merged main into this branch and pushed (9f27125). The branch was 269 commits behind. The merge had no conflicts.

@marufsharifi

Copy link
Copy Markdown
Contributor

BUG#1:

  1. Go to Workspaces → Domains → your domain → Domain members.
  2. Open the locked member.
  3. Go to Account → Troubleshoot.
  4. Enable Force offline.
  5. Return to the same member.
  6. Click Unlock account.
  7. Click Got it.
  8. Confirm Unlock account is greyed out while the request is queued.
  9. Press the page's back arrow to return to Domain members.
  10. Look at that member's row in the list.
  11. Open the member again, without reconnecting.

Expected result: The queued request remains visibly indicated when moving between the member details and the members list.

Actual result: The member's list row appears normal, with no pending styling, even though reopening the member details still shows the greyed-out unlock action.

PR:

RF-1.mov

Main:

RF-1-main

@marufsharifi

Copy link
Copy Markdown
Contributor

@MelvinBot find the RCA for this: #104188 (comment)

@MelvinBot

Copy link
Copy Markdown
Contributor Author

RCA for BUG#1 (list row shows no pending styling)

Root cause: This PR adds a new field-level pending action, unlockAccount, but only the details page reads it. The members list builds each row's pendingAction from a fixed set of fields. That set includes lockAccount but not unlockAccount, so the row stays normal while the request is queued.

  • Written: User.ts:1620. requestUnlockAccount sets domainPendingActions_<domainID>.member[accountID].unlockAccount = 'update'.
  • Read on details page: DomainMemberDetailsPage.tsx:92, passed to OfflineWithFeedback at line 222. That is why the button is greyed out.
  • Not read on list: DomainUtils.ts:179. getMemberCustomRowProps checks pendingAction ?? lockAccount ?? changeDomainSecurityGroup, with no unlockAccount. The PR added unlockAccountErrors to this function at line 168 but not the matching pending field.

Introduced by this PR. On main, requestUnlockAccount writes nothing optimistically. Main has no pending unlock state on either screen, so the normal row in the main screenshot is not the same bug.

Fix: In DomainUtils.ts:179, add accountIDPendingActions?.unlockAccount right after accountIDPendingActions?.lockAccount. Then add a DomainUtilsTest case: member[accountID].unlockAccount = 'update' should return pendingAction: 'update'. DomainMembersPage.tsx needs no change.

Investigation details
  • Lock account shows on the row because of the explicit lockAccount term at DomainUtils.ts:179. The value is set at User.ts:1483.
  • Add and remove member set the member-level pendingAction, which the first two terms of the chain already cover.
  • The list page passes this result as the row's pendingAction in DomainMembersPage.tsx:116.
  • I confirmed this by reading the code at PR head 9f27125. I did not run the fix in the app.

Next Steps: Reply with @MelvinBot <your feedback> to refine this analysis.


view run

@marufsharifi

Copy link
Copy Markdown
Contributor

@MelvinBot fix this: #104188 (comment)

Co-authored-by: Maruf Sharifi <marufsharifi@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Fixed BUG#1 in 34ba94a. The members list row now shows pending styling while an unlock request is queued.

DomainUtilsTest (77 tests), typecheck, lint, and oxfmt all pass. I did not check the row in the app.

@marufsharifi

Copy link
Copy Markdown
Contributor

@MelvinBot resolve the conflicts.

…equestState

Co-authored-by: Maruf Sharifi <marufsharifi@users.noreply.github.com>

# Conflicts:
#	src/pages/domain/Members/DomainMemberDetailsPage.tsx
@MelvinBot

Copy link
Copy Markdown
Contributor Author

I resolved the merge conflicts by merging main into this branch in 1211a95. The PR is now mergeable.

The only conflict was in the imports of src/pages/domain/Members/DomainMemberDetailsPage.tsx. I kept this PR's unlockRequestedSelector import and the new authTokenTypeSelector import from main. The app typecheck and lint-changed both pass.

This branch has not been deployed

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

Labels

Melvin-Test-Android Melvin-Test-Web Triggers Melvin to run the testing steps of the PR on web

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants