Skip to content

[Release 3.2] [Domain control] Enable, disable or reset their 2FA#82135

Merged
mountiny merged 53 commits intoExpensify:mainfrom
software-mansion-labs:war-in/add-2FA-toggle-to-member-details
Feb 25, 2026
Merged

[Release 3.2] [Domain control] Enable, disable or reset their 2FA#82135
mountiny merged 53 commits intoExpensify:mainfrom
software-mansion-labs:war-in/add-2FA-toggle-to-member-details

Conversation

@war-in
Copy link
Copy Markdown
Contributor

@war-in war-in commented Feb 11, 2026

Explanation of Change

PR implements Force two-factor authentication toggle and Reset two-factor authentication button

Fixed Issues

$ #79569
PROPOSAL:

Tests

  1. Open domain/<domainID>/members
  2. Click on member row
  3. Verify there are Force two-factor authentication toggle and Reset two-factor authentication entries in RHP
  4. Click on toggle
    4.1 When toggle was disabled - verify it's now enabled
    4.2 If toggle is enabled
  • and you don't have 2FA - verify it's disabled after the click
  • you have 2FA - verify that another screen with 2FA code input opens
    • enter invalid code and see an error
    • enter valid code and verify that the page is closed after the request finishes, and you're taken back to member details
  1. Click on the member who has 2FA setup
  2. Click on Reset two-factor authentication
  3. On 2FA code input page enter invalid code
  4. Verify that RHP closes and there is an error under selected member (Invalid code)
  5. Go to the details again and enter valid code
  6. Verify that this user doesn't have 2FA anymore
  7. Sign in as another admin that doesn't have 2FA and verify there is no Reset two-factor authentication item in member details

Offline tests

QA Steps

  1. Open domain/<domainID>/members
  2. Click on member row
  3. Verify there are Force two-factor authentication toggle and Reset two-factor authentication (if you have 2FA enabled) entries in RHP
  4. Click on toggle
    4.1 When toggle was disabled - verify it's now enabled
    4.2 If toggle is enabled
  • and you don't have 2FA - verify it's disabled after the click
  • you have 2FA - verify that another screen with 2FA code input opens
    • enter invalid code and see an error
    • enter valid code and verify that the page is closed after the request finishes, and you're taken back to member details
  1. Click on the member who has 2FA setup
  2. Click on Reset two-factor authentication
  3. On 2FA code input page enter invalid code
  4. Verify that RHP closes and there is an error under selected member (Invalid code)
  5. Go to the details again and enter valid code
  6. Verify that this user doesn't have 2FA anymore
  7. Sign in as another admin that doesn't have 2FA and verify there is no Reset two-factor authentication item in member details

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 there are no new alerts related to the canBeMissing param for useOnyx
  • I followed proper code patterns (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 shown in the product is localized by adding it to src/languages/* files and using the translation method
      • If any non-english text was added/modified, I used JaimeGPT to get English > Spanish translation. I then posted it in #expensify-open-source and it was approved by an internal Expensify engineer. Link to Slack message:
    • I verified all numbers, amounts, dates and phone numbers shown in the product are using the localization methods
    • 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)
    • I verified proper file naming conventions were followed for any new files or renamed files. All non-platform specific files are named after what they export and are not named "index.js". All platform-specific files are named for the platform the code supports as outlined in the README.
    • I verified the JSDocs style guidelines (in STYLE.md) were followed
  • 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)
  • I verified all code is DRY (the PR doesn't include any logic written more than once, with the exception of tests)
  • I verified any variables that can be defined as constants (ie. in CONST.ts or at the top of the file that uses the constant) are defined as such
  • I verified that if a function's arguments changed that all usages have also been updated correctly
  • 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 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 a new page is added, I verified it's using the ScrollView component to make it scrollable when more elements are added to the page.
  • 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

Reset 2FA flow:

Screen.Recording.2026-02-13.at.13.53.12.mov

Force 2FA flow:

Screen.Recording.2026-02-13.at.13.55.36.mov

war-in added 26 commits January 7, 2026 15:32
…' into war-in/add-2FA-toggle-to-member-details

# Conflicts:
#	src/ROUTES.ts
#	src/SCREENS.ts
#	src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
#	src/libs/Navigation/linkingConfig/RELATIONS/DOMAIN_TO_RHP.ts
#	src/libs/Navigation/linkingConfig/config.ts
#	src/libs/Navigation/types.ts
#	src/pages/domain/Members/DomainMembersPage.tsx
…to war-in/add-2FA-toggle-to-member-details

# Conflicts:
#	src/languages/en.ts
#	src/libs/Navigation/linkingConfig/RELATIONS/DOMAIN_TO_RHP.ts
#	src/libs/actions/Domain.ts
#	src/pages/domain/Admins/DomainAdminDetailsPage.tsx
#	src/pages/domain/BaseDomainMembersPage.tsx
…etails_page' into war-in/add-2FA-toggle-to-member-details

# Conflicts:
#	src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
#	src/pages/domain/Admins/DomainAdminDetailsPage.tsx
#	src/pages/domain/BaseDomainMemberDetailsComponent.tsx
…dd-2FA-toggle-to-member-details

# Conflicts:
#	src/ROUTES.ts
#	src/SCREENS.ts
#	src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
#	src/libs/Navigation/linkingConfig/RELATIONS/DOMAIN_TO_RHP.ts
#	src/libs/Navigation/linkingConfig/config.ts
#	src/libs/Navigation/types.ts
…dd-2FA-toggle-to-member-details

# Conflicts:
#	src/languages/en.ts
#	src/pages/domain/Members/DomainRequireTwoFactorAuthPage.tsx
…dd-2FA-toggle-to-member-details

# Conflicts:
#	src/libs/Navigation/linkingConfig/RELATIONS/DOMAIN_TO_RHP.ts
…dd-2FA-toggle-to-member-details

# Conflicts:
#	src/ROUTES.ts
#	src/languages/en.ts
#	src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
#	src/libs/Navigation/linkingConfig/RELATIONS/DOMAIN_TO_RHP.ts
#	src/pages/domain/BaseDomainMembersPage.tsx
#	src/pages/domain/Members/DomainMembersPage.tsx
#	src/types/onyx/DomainErrors.ts
#	src/types/onyx/DomainPendingActions.ts
…dd-2FA-toggle-to-member-details

# Conflicts:
#	src/pages/domain/Members/DomainMembersSettingsPage.tsx
#	src/pages/domain/Members/DomainRequireTwoFactorAuthPage.tsx
…dd-2FA-toggle-to-member-details

# Conflicts:
#	src/libs/actions/Domain.ts
#	src/pages/domain/BaseDomainMembersPage.tsx
#	src/pages/domain/Members/DomainMemberDetailsPage.tsx
#	src/pages/domain/Members/DomainMembersPage.tsx
…etails

# Conflicts:
#	src/pages/domain/Admins/DomainAdminsPage.tsx
#	src/pages/domain/Members/DomainMembersPage.tsx
#	src/pages/domain/Members/DomainMembersSettingsPage.tsx
#	src/pages/domain/Members/DomainRequireTwoFactorAuthPage.tsx
@melvin-bot
Copy link
Copy Markdown

melvin-bot bot commented Feb 11, 2026

Hey, I noticed you changed src/languages/en.ts in a PR from a fork. For security reasons, translations are not generated automatically for PRs from forks.

If you want to automatically generate translations for other locales, an Expensify employee will have to:

  1. Look at the code and make sure there are no malicious changes.
  2. Run the Generate static translations GitHub workflow. If you have write access and the K2 extension, you can simply click: [this button]

Alternatively, if you are an external contributor, you can run the translation script locally with your own OpenAI API key. To learn more, try running:

npx ts-node ./scripts/generateTranslations.ts --help

Typically, you'd want to translate only what you changed by running npx ts-node ./scripts/generateTranslations.ts --compare-ref main

…etails

# Conflicts:
#	src/pages/domain/BaseDomainMembersPage.tsx
#	src/pages/domain/Members/DomainMembersPage.tsx
@war-in
Copy link
Copy Markdown
Contributor Author

war-in commented Feb 24, 2026

@situchan fixed! and merged main

@situchan
Copy link
Copy Markdown
Contributor

job 7 is broken on main but job2 is related to this PR

@situchan
Copy link
Copy Markdown
Contributor

situchan commented Feb 24, 2026

Bug: Offline case is not handled in disable / reset 2fa flow

Screen.Recording.2026-02-25.at.2.23.36.AM.mov

I think we should follow same offline pattern as account settings:

Screenshot 2026-02-25 at 2 23 17 AM

@war-in
Copy link
Copy Markdown
Contributor Author

war-in commented Feb 25, 2026

Bug: Offline case is not handled in disable / reset 2fa flow

@situchan fixed!

Screen.Recording.2026-02-25.at.10.43.51.mov

Copy link
Copy Markdown
Contributor

@situchan situchan left a comment

Choose a reason for hiding this comment

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

:shipit:

@melvin-bot melvin-bot bot requested a review from mountiny February 25, 2026 10:15
@mountiny mountiny requested review from Copilot and removed request for sobitneupane February 25, 2026 10:49
@mountiny mountiny merged commit 022adf1 into Expensify:main Feb 25, 2026
29 checks passed
@github-project-automation github-project-automation bot moved this from Ready for Review to Done in Bring Domain Control to NewDot Feb 25, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds domain-admin controls for member-level 2FA management by introducing a per-member “Force two-factor authentication” toggle (via exemption list management) and a “Reset two-factor authentication” action, along with supporting API commands, navigation routes, Onyx types, translations, and unit tests.

Changes:

  • Added new domain actions/API commands for managing 2FA-exempt emails and resetting a member’s 2FA, including optimistic/pending/error handling.
  • Added new member 2FA modal pages and updated member details UI to expose the toggle + reset entry; refactored domain 2FA code-entry UI into a shared base page.
  • Updated Onyx types and translations; added/expanded unit tests for selectors and domain actions.

Reviewed changes

Copilot reviewed 35 out of 35 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/unit/selectors/AccountTest.ts Adds unit tests for the new requiresTwoFactorAuthSelector.
tests/actions/DomainTest.ts Adds tests covering new domain actions for 2FA exemption + reset flows.
src/types/onyx/DomainPendingActions.ts Extends pending actions to include member-level twoFactorAuthExemptEmails and makes base pendingAction optional.
src/types/onyx/DomainErrors.ts Adds twoFactorAuthExemptEmailsError to member errors shape.
src/types/onyx/CardFeeds.ts Extends domain settings type with twoFactorAuthExemptEmails.
src/selectors/Account.ts Adds requiresTwoFactorAuthSelector.
src/pages/domain/Members/TwoFactorAuth/DomainMemberResetTwoFactorAuthPage.tsx New page to submit admin 2FA code to reset a member’s 2FA.
src/pages/domain/Members/TwoFactorAuth/DomainMemberForceTwoFactorAuthPage.tsx New page to submit admin 2FA code when exempting a member from forced 2FA.
src/pages/domain/Members/DomainRequireTwoFactorAuthPage.tsx Refactors domain-level disable flow to reuse the new shared base 2FA-code page.
src/pages/domain/Members/DomainMembersSettingsPage.tsx Switches translation keys to domain.common.* for the domain-level force-2FA toggle.
src/pages/domain/Members/DomainMembersPage.tsx Ensures member-row error merging includes the new 2FA exemption error field.
src/pages/domain/Members/DomainMemberDetailsPage.tsx Adds per-member force-2FA toggle and reset-2FA menu item in the member details RHP.
src/pages/domain/BaseDomainRequireTwoFactorAuthPage.tsx Introduces shared 2FA-code entry page used by domain + member flows.
src/libs/actions/Domain.ts Implements new write actions: set 2FA-exempt email, clear related errors, reset member 2FA; updates error translation key.
src/libs/Navigation/types.ts Adds params for the new member 2FA screens.
src/libs/Navigation/linkingConfig/config.ts Registers deeplink paths for new member 2FA routes.
src/libs/Navigation/linkingConfig/RELATIONS/DOMAIN_TO_RHP.ts Adds new member 2FA screens to the Domain→RHP relations.
src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx Registers the new member 2FA pages in the settings modal stack.
src/libs/DomainUtils.ts Updates domain-member details error detection to include 2FA exemption errors.
src/libs/API/types.ts Adds new WRITE commands for exempt-email management and member 2FA reset.
src/libs/API/parameters/index.ts Exports new API parameter types.
src/libs/API/parameters/SetTwoFactorAuthExemptEmailForDomainParams.ts Defines request params for exempt-email toggle write.
src/libs/API/parameters/ResetDomainMemberTwoFactorAuthParams.ts Defines request params for member 2FA reset write.
src/languages/zh-hans.ts Adds domain.common.resetTwoFactorAuth and moves force-2FA strings under domain.common.
src/languages/pt-BR.ts Adds domain.common.resetTwoFactorAuth and moves force-2FA strings under domain.common.
src/languages/pl.ts Adds domain.common.resetTwoFactorAuth and moves force-2FA strings under domain.common.
src/languages/nl.ts Adds domain.common.resetTwoFactorAuth and moves force-2FA strings under domain.common.
src/languages/ja.ts Adds domain.common.resetTwoFactorAuth and moves force-2FA strings under domain.common.
src/languages/it.ts Adds domain.common.resetTwoFactorAuth and moves force-2FA strings under domain.common.
src/languages/fr.ts Adds domain.common.resetTwoFactorAuth and moves force-2FA strings under domain.common.
src/languages/es.ts Adds domain.common.resetTwoFactorAuth and moves force-2FA strings under domain.common.
src/languages/en.ts Adds domain.common.resetTwoFactorAuth and moves force-2FA strings under domain.common.
src/languages/de.ts Adds domain.common.resetTwoFactorAuth and moves force-2FA strings under domain.common.
src/SCREENS.ts Adds new screen constants for member force/reset 2FA flows.
src/ROUTES.ts Adds new routes for member force/reset 2FA flows.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +60 to +64
<HeaderWithBackButton
title={translate('twoFactorAuth.disableTwoFactorAuth')}
onBackButtonPress={onBackButtonPress}
shouldDisplayHelpButton={false}
/>
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

BaseDomainRequireTwoFactorAuthPage hardcodes the header title to twoFactorAuth.disableTwoFactorAuth, but this component is also used for member 2FA flows (force/reset). That will show an incorrect/misleading title in those contexts. Consider making the title configurable via props (e.g. a headerTitle/headerTitleTranslationKey prop) and passing the appropriate copy from each caller.

Copilot uses AI. Check for mistakes.
Comment on lines +88 to +94
<Button
success
large
text={translate('common.disable')}
isLoading={!!pendingAction}
onPress={() => baseTwoFactorAuthRef.current?.validateAndSubmitForm()}
/>
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

The submit button label is hardcoded to common.disable, which doesn’t fit all uses of this shared page (e.g. “Reset two-factor authentication”). Making the button text configurable (and optionally the button style) will avoid incorrect copy when reusing this component.

Copilot uses AI. Check for mistakes.
Comment on lines +710 to +724
it('adds targetEmail to exempt emails in optimisticData when force2FA is false and no twoFactorAuthCode is provided', () => {
const apiWriteSpy = jest.spyOn(require('@libs/API'), 'write').mockImplementation(() => Promise.resolve());

setTwoFactorAuthExemptEmailForDomain(domainAccountID, accountID, exemptEmails, targetEmail, false);

expect(apiWriteSpy).toHaveBeenCalledWith(
WRITE_COMMANDS.SET_TWO_FACTOR_AUTH_EXEMPT_EMAIL_FOR_DOMAIN,
expect.objectContaining({enabled: true}),
expect.objectContaining({
optimisticData: expect.arrayContaining([
expect.objectContaining({
key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${domainAccountID}`,
value: {settings: {twoFactorAuthExemptEmails: [...exemptEmails, targetEmail]}},
}),
]),
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

This test currently expects targetEmail to be appended even though exemptEmails already contains it, which enshrines duplicate entries in twoFactorAuthExemptEmails. If the action is made idempotent by de-duping, update this expectation to assert the email is present only once.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +8
type ResetDomainMemberTwoFactorAuth = {
domainAccountID: number;
targetAccountID: number;
targetEmail: string;
twoFactorAuthCode: string;
};

export default ResetDomainMemberTwoFactorAuth;
Copy link

Copilot AI Feb 25, 2026

Choose a reason for hiding this comment

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

The exported default type name (ResetDomainMemberTwoFactorAuth) doesn’t match the filename / re-export name (ResetDomainMemberTwoFactorAuthParams). Renaming the type to ResetDomainMemberTwoFactorAuthParams will make TS errors and IDE symbol search clearer and keep parameter types consistent.

Suggested change
type ResetDomainMemberTwoFactorAuth = {
domainAccountID: number;
targetAccountID: number;
targetEmail: string;
twoFactorAuthCode: string;
};
export default ResetDomainMemberTwoFactorAuth;
type ResetDomainMemberTwoFactorAuthParams = {
domainAccountID: number;
targetAccountID: number;
targetEmail: string;
twoFactorAuthCode: string;
};
export default ResetDomainMemberTwoFactorAuthParams;

Copilot uses AI. Check for mistakes.
@OSBotify
Copy link
Copy Markdown
Contributor

✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release.

@github-actions
Copy link
Copy Markdown
Contributor

🚧 @mountiny has triggered a test Expensify/App build. You can view the workflow run here.

@OSBotify
Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/mountiny in version: 9.3.26-0 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

@izarutskaya
Copy link
Copy Markdown

Hi @war-in We don't see Reset two-factor authentication. Do we need any special account settings?

image

@war-in
Copy link
Copy Markdown
Contributor Author

war-in commented Feb 26, 2026

Hi @war-in We don't see Reset two-factor authentication. Do we need any special account settings?

@izarutskaya yes, you need to have 2FA enabled in the Settings -> Security -> Two-factor authentication. I'll add that to the test steps, sorry!

@izarutskaya
Copy link
Copy Markdown

@war-in Probably, we can't test it for now as we can't edit domain group settings or members still (details here) Could you please check this internally? Thank you in advance

@mountiny
Copy link
Copy Markdown
Contributor

Let's retest this once you are allowed

@war-in
Copy link
Copy Markdown
Contributor Author

war-in commented Feb 27, 2026

From my testing on staging, it looks like the Reset two-factor authentication is working correctly

@OSBotify
Copy link
Copy Markdown
Contributor

🚀 Deployed to production by https://github.com/puneetlath in version: 9.3.26-8 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

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

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

10 participants