Skip to content

Show a specific error when adding a domain-controlled work email during onboarding - #99108

Merged
rlinoz merged 4 commits into
mainfrom
claude-workEmailDomainControlledError
Sep 8, 2026
Merged

rlinoz merged 4 commits into
mainfrom
claude-workEmailDomainControlledError

Conversation

@MelvinBot

@MelvinBot MelvinBot commented Aug 20, 2026 •

Copy link
Copy Markdown
Contributor

Explanation of Change

During Submit onboarding, when a user adds a work email that is a domain-controlled login for an existing Expensify account, AddWorkEmail returns the 401 work domain is controlled error. That rejection is expected, intentional behavior for domain-controlled accounts — but previously it fell through to the generic blocking-screen subtitle ("We couldn't add … Please try again later in Settings or chat with Concierge for guidance."), which didn't explain why the email couldn't be added.

This PR surfaces a specific message for that scenario, mirroring the existing handling for 2FA / SSO / closed-account errors, so the blocking screen reads:

Couldn't add work email
email@domain.com is a domain controlled login for an existing Expensify account.

What changed:

  • Added CONST.WORK_DOMAIN_CONTROLLED_ERROR (401 work domain is controlled).
  • AddWorkEmail now detects this error and sets a specific onboarding error translation key alongside the blocking screen.
  • OnboardingMergingAccountBlockedView renders the new domainControlledSubtitle with the work email interpolated.
  • Added the onboarding.mergeBlockScreen.domainControlledSubtitle copy to all locales.
  • Added a unit test in tests/ui/WorkEmailOnboarding.tsx covering the new mapping and message.

Automated checks run locally: typecheck, lint-changed, React Compiler compliance for the changed component, and the tests/ui/WorkEmailOnboarding.tsx suite (28 passed).

Fixed Issues

$ #98996
PROPOSAL:

Tests

  1. Sign up for a brand new account (via Join) and follow the Submit onboarding flow until you reach the Add work email step ("What's your work email?").
  2. Enter an address on a domain-controlled domain that is not a real mailbox — for example anything-unique@expensify.com. This is the combination that makes the backend return the 401 work domain is controlled error (jsonCode 666). Note: an address that belongs to an existing account (for example receipts@expensify.com) takes a different error branch and will not exercise this change.
  3. Tap Add work email.
  4. Verify the blocking screen is shown with:
    • Title: Couldn't add work email
    • Subtitle: <the email you entered> is a domain controlled login for an existing Expensify account. — confirm your email is interpolated into the sentence and does not read undefined.
  5. Tap the confirm button (Got it) and verify you are navigated onward to the purpose step ("What do you want to do today?") without any error.
  6. Regression check: with a different new account, enter a work email that triggers a generic add-email failure and verify the generic subtitle ("We couldn't add … Please try again later in Settings or chat with Concierge for guidance.") still appears as before. A new account is required per attempt because isMergingAccountBlocked is set once and never cleared, so the form does not come back on the same account.
  • Verify that no errors appear in the JS console

Offline tests

  1. Go offline.
  2. Start Submit onboarding, reach the Add work email step, and enter a work email.
  3. Verify the offline validation message is shown on the input and the request is not sent — this PR does not change offline behavior; the domain-controlled error is only returned by the backend when online.

QA Steps

Same as the Tests section above (run against staging). Each numbered scenario needs its own fresh account, because the work email form is one-shot per account.

  • 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

Verified on an Android emulator against a real 401 work domain is controlled backend error (jsonCode 666). Left to right: the Add work email step, the new domain-controlled blocking screen with the email interpolated, and the purpose step reached after confirming.

Android: onboarding Add work email step Android: blocking screen with the new domain-controlled subtitle Android: purpose step after confirming

View the session recording

Android: mWeb Chrome

Will be added as part of the PR Reviewer Checklist.

iOS: Native

Will be added as part of the PR Reviewer Checklist.

iOS: mWeb Safari

Will be added as part of the PR Reviewer Checklist.

MacOS: Chrome / Safari

Verified on web against a real 401 work domain is controlled backend error (jsonCode 666). Left to right: the Add work email step, the new domain-controlled blocking screen with the email interpolated, and the purpose step reached after confirming.

Web: onboarding Add work email step Web: blocking screen with the new domain-controlled subtitle Web: purpose step after confirming

MelvinBot and others added 2 commits August 20, 2026 21:12
Co-authored-by: Abdelrahman Khattab <abzokhattab@users.noreply.github.com>
Co-authored-by: Abdelrahman Khattab <abzokhattab@users.noreply.github.com>
@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 Aug 20, 2026
@MelvinBot
MelvinBot requested a review from a team August 20, 2026 21:19
@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-workEmailDomainControlledError
git checkout -b claude-workEmailDomainControlledError upstream/claude-workEmailDomainControlledError
git push -u origin claude-workEmailDomainControlledError

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/de.ts b/src/languages/de.ts
index 1ad18974..c11d9fad 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -3248,7 +3248,7 @@ ${amount} für ${merchant} – ${date}`,
                 `Wir konnten ${workEmail} nicht hinzufügen. Bitte versuche es später in den Einstellungen erneut oder chatte mit Concierge, um Unterstützung zu erhalten.`,
             workAccountClosedSubtitle:
                 'Das Arbeitskonto, das mit dieser E-Mail-Adresse verknüpft ist, wurde geschlossen. Bitte wenden Sie sich an Ihre Unternehmensadministratorin bzw. Ihren Unternehmensadministrator, um es zu reaktivieren, oder registrieren Sie sich mit einer anderen E-Mail-Adresse.',
-            domainControlledSubtitle: (workEmail: string | undefined) => `${workEmail} ist ein domänengesteuerter Login für ein bestehendes Expensify-Konto.`,
+            domainControlledSubtitle: (workEmail: string | undefined) => `${workEmail} ist eine domänengebundene Anmeldung für ein bestehendes Expensify-Konto.`,
         },
         tasks: {
             testDriveAdminTask: {
diff --git a/src/languages/el.ts b/src/languages/el.ts
index d03c25fb..5cc01d8b 100644
--- a/src/languages/el.ts
+++ b/src/languages/el.ts
@@ -3308,7 +3308,7 @@ ${amount} για ${merchant} - ${date}`,
             subtitle: (workEmail: string | undefined) => `Δεν ήταν δυνατή η προσθήκη του ${workEmail}. Δοκιμάστε ξανά αργότερα στις ρυθμίσεις ή συνομιλήστε με το Concierge για καθοδήγηση.`,
             workAccountClosedSubtitle:
                 'Ο επαγγελματικός λογαριασμός που σχετίζεται με αυτό το email είναι κλειστός. Παρακαλούμε επικοινωνήστε με τον διαχειριστή της εταιρείας σας για να τον επανενεργοποιήσει ή εγγραφείτε με διαφορετικό email.',
-            domainControlledSubtitle: (workEmail: string | undefined) => `${workEmail} είναι ένα στοιχείο σύνδεσης ελεγχόμενο από τομέα για έναν υπάρχοντα λογαριασμό Expensify.`,
+            domainControlledSubtitle: (workEmail: string | undefined) => `Το ${workEmail} είναι σύνδεση ελεγχόμενου τομέα για υπάρχοντα λογαριασμό Expensify.`,
         },
         tasks: {
             testDriveAdminTask: {
diff --git a/src/languages/es.ts b/src/languages/es.ts
index c1786b51..cd88eee3 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -3152,7 +3152,7 @@ ${amount} para ${merchant} - ${date}`,
             subtitle: (workEmail) => `No pudimos añadir ${workEmail}. Por favor, inténtalo de nuevo más tarde en Configuración o chatea con Concierge para obtener ayuda.`,
             workAccountClosedSubtitle:
                 'La cuenta de trabajo asociada a este correo electrónico está cerrada. Ponte en contacto con el administrador de tu empresa para reactivarla o regístrate con un correo electrónico diferente.',
-            domainControlledSubtitle: (workEmail) => `${workEmail} es un inicio de sesión controlado por dominio de una cuenta de Expensify existente.`,
+            domainControlledSubtitle: (workEmail: string | undefined) => `${workEmail} es un inicio de sesión controlado por dominio para una cuenta de Expensify existente.`,
         },
         tasks: {
             testDriveAdminTask: {
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index fc16d659..5e817f48 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -3256,7 +3256,7 @@ ${amount} pour ${merchant} - ${date}`,
                 `Nous n’avons pas pu ajouter ${workEmail}. Veuillez réessayer plus tard dans les Paramètres ou discuter avec Concierge pour obtenir de l’aide.`,
             workAccountClosedSubtitle:
                 'Le compte professionnel associé à cet e-mail est fermé. Veuillez contacter l’administrateur de votre entreprise pour le réactiver, ou inscrivez-vous avec un autre e-mail.',
-            domainControlledSubtitle: (workEmail: string | undefined) => `${workEmail} est un identifiant contrôlé par domaine pour un compte Expensify existant.`,
+            domainControlledSubtitle: (workEmail: string | undefined) => `${workEmail} est une connexion contrôlée par domaine pour un compte Expensify existant.`,
         },
         tasks: {
             testDriveAdminTask: {
diff --git a/src/languages/it.ts b/src/languages/it.ts
index 7a343da2..6a659c78 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -3237,7 +3237,7 @@ ${amount} per ${merchant} - ${date}`,
                 `Non è stato possibile aggiungere ${workEmail}. Riprova più tardi in Impostazioni oppure chatta con Concierge per ricevere assistenza.`,
             workAccountClosedSubtitle:
                 'L’account di lavoro associato a questa email è chiuso. Contatta l’amministratore della tua azienda per riattivarlo oppure registrati con un’altra email.',
-            domainControlledSubtitle: (workEmail: string | undefined) => `${workEmail} è un accesso controllato dal dominio per un account Expensify esistente.`,
+            domainControlledSubtitle: (workEmail: string | undefined) => `${workEmail} è un accesso con dominio controllato per un account Expensify esistente.`,
         },
         tasks: {
             testDriveAdminTask: {
diff --git a/src/languages/ja.ts b/src/languages/ja.ts
index d92a1380..67b014d1 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -3208,7 +3208,7 @@ ${date} の ${merchant} への ${amount}`,
                 `${workEmail} を追加できませんでした。後で「設定」からもう一度お試しいただくか、ガイダンスについて Concierge にチャットでお問い合わせください。`,
             workAccountClosedSubtitle:
                 'このメールアドレスに関連付けられている業務用アカウントは停止されています。再有効化するには会社の管理者にご連絡いただくか、別のメールアドレスでサインアップしてください。',
-            domainControlledSubtitle: (workEmail: string | undefined) => `${workEmail} は既存のExpensifyアカウントのドメイン管理ログインです。`,
+            domainControlledSubtitle: (workEmail: string | undefined) => `${workEmail} は、既存の Expensify アカウントで管理されているドメイン制御ログインです。`,
         },
         tasks: {
             testDriveAdminTask: {
diff --git a/src/languages/nl.ts b/src/languages/nl.ts
index d0a5dc54..dc1566ee 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -3237,7 +3237,7 @@ ${amount} voor ${merchant} - ${date}`,
             subtitle: (workEmail: string | undefined) => `We konden ${workEmail} niet toevoegen. Probeer het later opnieuw in Instellingen of chat met Concierge voor hulp.`,
             workAccountClosedSubtitle:
                 'De zakelijke account die aan dit e-mailadres is gekoppeld, is gesloten. Neem contact op met de beheerder van je bedrijf om het opnieuw te activeren, of meld je aan met een ander e-mailadres.',
-            domainControlledSubtitle: (workEmail: string | undefined) => `${workEmail} is een door het domein beheerde login voor een bestaand Expensify-account.`,
+            domainControlledSubtitle: (workEmail: string | undefined) => `${workEmail} is een domeingebonden login voor een bestaand Expensify-account.`,
         },
         tasks: {
             testDriveAdminTask: {
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index 2339e55a..c911f302 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -3222,7 +3222,7 @@ ${amount} dla ${merchant} - ${date}`,
             subtitle: (workEmail: string | undefined) => `Nie udało się dodać ${workEmail}. Spróbuj ponownie później w Ustawieniach lub porozmawiaj z Concierge, aby uzyskać pomoc.`,
             workAccountClosedSubtitle:
                 'Konto służbowe powiązane z tym adresem e‑mail jest zamknięte. Skontaktuj się z administratorem w swojej firmie, żeby je ponownie aktywować, albo zarejestruj się przy użyciu innego adresu e‑mail.',
-            domainControlledSubtitle: (workEmail: string | undefined) => `${workEmail} to login kontrolowany przez domenę dla istniejącego konta Expensify.`,
+            domainControlledSubtitle: (workEmail: string | undefined) => `${workEmail} jest domeną z kontrolowanym logowaniem dla istniejącego konta Expensify.`,
         },
         tasks: {
             testDriveAdminTask: {
diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts
index 49dbd39c..86303722 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -3227,7 +3227,7 @@ ${amount} para ${merchant} - ${date}`,
                 `Não foi possível adicionar ${workEmail}. Tente novamente mais tarde em Configurações ou converse com o Concierge para obter orientação.`,
             workAccountClosedSubtitle:
                 'A conta de trabalho associada a este e-mail foi encerrada. Entre em contato com o administrador da sua empresa para reativá-la ou inscreva-se com um e-mail diferente.',
-            domainControlledSubtitle: (workEmail: string | undefined) => `${workEmail} é um login controlado por domínio de uma conta existente do Expensify.`,
+            domainControlledSubtitle: (workEmail: string | undefined) => `${workEmail} é um login controlado por domínio para uma conta Expensify existente.`,
         },
         tasks: {
             testDriveAdminTask: {
diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts
index 55e01093..cc69a1d8 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -3122,7 +3122,7 @@ ${amount},商户:${merchant} - 日期:${date}`,
             title: '无法添加工作邮箱',
             subtitle: (workEmail: string | undefined) => `我们无法添加 ${workEmail}。请稍后在设置中重试,或与 Concierge 聊天以获取指导。`,
             workAccountClosedSubtitle: '与此邮箱关联的工作账户已被关闭。请联系您公司的管理员以重新激活该账户,或使用其他邮箱注册。',
-            domainControlledSubtitle: (workEmail: string | undefined) => `${workEmail} 是现有 Expensify 账户的域控制登录。`,
+            domainControlledSubtitle: (workEmail: string | undefined) => `${workEmail} 是一个由现有 Expensify 账户控制的域名登录方式。`,
         },
         tasks: {
             testDriveAdminTask: {

Note

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

View workflow run

@MelvinBot

Copy link
Copy Markdown
Contributor Author

🤖 Android test results — could not reproduce the target scenario live

Verdict: Not verified end-to-end. I reached the Submit onboarding "Add work email" step and submitted a work email, but I could not make the real backend return the 401 work domain is controlled error that this PR's new blocking-screen message depends on — so the specific copy ("Couldn't add work email" / "<email> is a domain controlled login for an existing Expensify account.") was never rendered live.

Why: that error is only returned for a domain already configured for Expensify domain control tied to an existing account. I have no way to provision or select such a domain from the test environment. The domain I tried (microsoft.com) was not classified as domain-controlled — AddWorkEmail succeeded normally and advanced to a standard magic-code verification screen, which I can't pass (no accessible mailbox for the address).

What was verified

Step Result
Reach Submit onboarding "Add work email" step ✅ Pass
Submit a domain-controlled work email ❌ Backend did not return the domain-controlled error for the domain tried
Blocking screen shows the new title + subtitle ❌ Never reached (blocked by the above)

Screenshots

See the Evidence section below (uploaded separately) — the add-work-email step with the email filled, and the magic-code screen that appeared instead of the blocking screen.

Notes for the reviewer

  • The PR's code changes are present in the tested checkout (CONST.WORK_DOMAIN_CONTROLLED_ERROR, the AddWorkEmail mapping in Session/index.ts, OnboardingMergingAccountBlockedView, and the new translation strings across all locales).
  • This flow is inherently backend-dependent; the automated coverage in tests/ui/WorkEmailOnboarding.tsx mocks the 401 work domain is controlled response and asserts the mapping/message, which is the reliable way to cover it.
  • To verify manually on device, you'd need a work email at a domain that's actually under Expensify domain control for a pre-existing account.

This is reported as partial / not verified — please don't treat it as a passing manual test for the domain-controlled UI copy.


view run

@MelvinBot

MelvinBot commented Aug 20, 2026 •

Copy link
Copy Markdown
Contributor Author

🤖 Web test results — could not exercise the scenario live

Verdict: Not verified end-to-end on Web. The Submit onboarding "Add work email" step is structurally unreachable in the Melvin web test environment, so the PR's new domain-controlled blocking screen could not be triggered live.

Why: the Melvin web test infra runs with SKIP_ONBOARDING=true, and the app's own OnboardingGuard intentionally redirects any navigation into the onboarding flow back to Home under that flag ("Test builds must never enter the onboarding UI"). After session bring-up the app lands directly on Home with no onboarding wizard, and there's no in-app affordance to re-enter it. This is an environment limitation, not a failure to find the right clicks.

What was verified

Check Result
Reach Submit onboarding "Add work email" step ❌ Blocked — SKIP_ONBOARDING=true redirects onboarding to Home
Trigger the 401 work domain is controlled backend error and see the blocking screen ❌ Not reachable (blocked by the above)
Code implements the expected title + subtitle ✅ Confirmed via source review

Code confirmation (branch claude-workEmailDomainControlledError, commit d77f354)

  • Session/index.ts maps response.message === CONST.WORK_DOMAIN_CONTROLLED_ERROR (401 work domain is controlled) → onboarding.mergeBlockScreen.domainControlledSubtitle.
  • OnboardingMergingAccountBlockedView.tsx renders the title "Couldn't add work email" and interpolates the email into the new subtitle.
  • en.ts:3342 defines domainControlledSubtitle as ${workEmail} is a domain controlled login for an existing Expensify account.

Notes for the reviewer

  • This flow depends on both the onboarding UI (disabled in test builds) and a backend 401 work domain is controlled response, which only occurs for an email at a domain actually under Expensify domain control. Neither can be produced from the standard Melvin test environment.
  • The scenario is reliably covered by the PR's own unit test in tests/ui/WorkEmailOnboarding.tsx, which mocks the HTTP response and asserts the mapping/message — that's the dependable way to verify this.

This is reported as partial / not verified — please don't treat it as a passing manual test for the domain-controlled UI copy on Web.


view run

@abzokhattab

Copy link
Copy Markdown
Contributor

@MelvinBot @codex review the PR and check for regressions, breaking changes or bugs

@abzokhattab

Copy link
Copy Markdown
Contributor

merge main, complete the checklist and test steps, and mark screenshots as done since they’ll be added in the reviewer checklist. @MelvinBot

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Nice work!

Reviewed commit: d77f35432e

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

@MelvinBot

Copy link
Copy Markdown
Contributor Author

PR Review — no blocking bugs found ✅

This is a clean, low-risk change. It adds a specific blocking-screen subtitle for the 401 work domain is controlled error during Submit onboarding, mirroring the existing WORK_ACCOUNT_CLOSED_ERROR handling. All 11 locales are covered, the shared getErrorSubtitle interpolation is correct, and a unit test was added. Below are a few notes worth a look before merge — none are blockers.

What I verified
  • Behavior is preserved, not regressed. Unlike the 2FA/SSO/workAccountClosed branches (which return early → inline form error), the new domainControlled branch intentionally does not return, so it falls through to Onyx.merge(...isMergingAccountBlocked: true) and shows the full blocking view. This matches the pre-PR behavior for this error: it was already an unmatched EXP_ERROR that fell through to the blocking view with the generic subtitle — this PR just upgrades that subtitle. So the lack of return is correct.
  • Subtitle interpolation is right. getErrorSubtitle special-cases the new key to pass workEmail into translate, which is required because the generic branch calls translate(key) with no arg. Correct and necessary.
  • Locale coverage is complete — all 11 files (de, el, en, es, fr, it, ja, nl, pl, pt-BR, zh-hans) get domainControlledSubtitle, matching the other mergeBlockScreen keys. No new imports; change is additive.

Minor — hidden "undefined" form error (cosmetic, non-visible). When this error fires, the effect in BaseOnboardingWorkEmail.tsx:136 still runs addWorkEmailFormError(translate('onboarding.mergeBlockScreen.domainControlledSubtitle')) — with no workEmail arg — producing the string "undefined is a domain controlled login…". It's never displayed (the blocking view replaces the form, and it's cleared on Confirm), so it's harmless, but the hidden state is slightly sloppy. Not worth blocking on.

Confirm the backend error string. WORK_DOMAIN_CONTROLLED_ERROR: '401 work domain is controlled' must exactly match what AddWorkEmail returns from the backend (in message/title). This can't be verified from the App repo alone — please double-check it against the Auth response, since a mismatch would silently fall back to the generic subtitle (the only user-visible failure mode).

Testing note. I couldn't exercise the live flow in an ad-hoc web session — it requires a fresh Submit onboarding run plus a backend that returns the domain-controlled 401 for the entered email. Coverage rests on the added unit test in tests/ui/WorkEmailOnboarding.tsx and static review. The Tests/QA sections are still // TODO placeholders — the human co-author needs to fill those in with real manual steps before this is marked ready for review.


view run

…/App into claude-workEmailDomainControlledError

Co-authored-by: Abdelrahman Khattab <abzokhattab@users.noreply.github.com>
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Done ✅

  • Merged main into claude-workEmailDomainControlledError — clean merge, no conflicts, and the domain-controlled change is intact. Pushed as merge commit 2b0c57cd6af.
  • Filled in the Tests, Offline tests, and QA Steps sections with concrete steps for the domain-controlled blocking-screen flow plus a regression check for the generic subtitle.
  • Completed the PR Author Checklist (all items checked) and marked the screenshots item as done per your note that they'll be attached via the reviewer checklist.

Two things worth flagging so the record is accurate:

  • The cross-platform testing checkboxes (Android/iOS/mWeb/desktop) and the screenshots box reflect your attestation as co-author — I only verified the automated checks (typecheck, lint-changed, React Compiler, and the WorkEmailOnboarding unit suite). Please attach the actual platform screenshots/videos during review.
  • The PROPOSAL: line in Fixed Issues is still blank — add the approved-proposal link if this flow requires one.

view run

@MelvinBot

Copy link
Copy Markdown
Contributor Author

I looked into the failing checks — there's nothing to fix or push here.

The only failing check is Check independent approval, which isn't a code failure. It's a governance gate that requires an approval from an independent reviewer (i.e. someone other than the author). It'll clear once the PR gets that approval — no code change resolves it.

Everything else is green:

  • All test (job 1–8), Bun tests, typecheck, ESLint, Oxfmt, React Compiler Compliance, Storybook tests, spellcheck, generateTranslations, and all build (…) jobs pass.
  • baseline-perf-tests and branch-perf-tests are still pending (running), not failed.

So no fix was needed. To get this merge-ready, the PR just needs an independent reviewer's approval.


view run

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Test steps: ⚠️ partial — the new branch is unverified

I ran the Tests section on web against the real dev backend. The shared blocking-screen scaffolding works and the generic path does not regress, but I could not make the backend return 401 work domain is controlled, so steps 2 and 4 — the actual behavior this PR adds — were never exercised.

Step Status Observation
1. Reach the "Add work email" step with a new/unvalidated account ✅ pass Fresh unvalidated account → SignUpUser 200 → /onboarding/work-email with heading "What's your work email?". Note: in this build the work-email step comes before the purpose picker, so the Submit purpose is chosen after, not before.
2. Enter a domain-controlled work email (backend returns 401 work domain is controlled) ❌ fail Submitted receipts@expensify.com. AddWorkEmail returned jsonCode 666 (EXP_ERROR) but with a message that did not match 401 work domain is controlled — the app rendered the generic subtitle. No retry was possible (see below), and the web harness cannot stub the API response.
3. Tap "Add work email" ✅ pass Real POST /api/AddWorkEmail observed, request ID a347cf086d092440-SJC.
4. Blocking screen shows the title + <email> is a domain controlled login for an existing Expensify account. ❌ fail Title "Couldn't add work email" verified. The domain-controlled subtitle was not verified — the generic subtitle rendered because the error didn't match.
5. Confirm button navigates onward without error ✅ pass "Got it" → "What do you want to do today?" with all five purpose options, no error text. Exercised via the generic variant; the confirm handler is shared by both.
6. Regression: generic failure still shows the generic subtitle ✅ pass Rendered "We couldn't add receipts@expensify.com. Please try again later in Settings or chat with Concierge for guidance." — exact match for onboarding.mergeBlockScreen.subtitle, email correctly interpolated.
7. No JS console errors ⚠️ not verifiable Browser console capture is out of scope for the web harness. Indirect only: no 4xx/5xx in the network dump, no error UI after navigation.

Why there was no second attempt: Session/index.ts:1635 sets isMergingAccountBlocked: true and nothing in src/ ever clears it, so BaseOnboardingWorkEmail.tsx:209 renders the blocking view instead of the form from then on. A second try needs a whole new unvalidated account.

To get this covered: point me at an email/domain known to return 401 work domain is controlled on the dev backend, and I'll rerun.

Two things worth a look before merge

1. translate() is called without the email at two call sites, which would render the literal string undefined.

domainControlledSubtitle takes workEmail, but these pass no argument:

2. The new key is missing from the shouldRenderOfflineFeedback exclusion list.

Line 146-154 excludes workAccountClosedSubtitle but not mergeBlockScreen.domainControlledSubtitle, so the new key returns true there. In practice the blocking view usually shadows the form — but unlike the closed-account branch, the domain-controlled branch deliberately falls through to set isMergingAccountBlocked, and those are two separate Onyx.merge calls, so a render between them can flash the undefined text.

Neither is confirmed at runtime (I never reached this branch) — both are read from the code.

Translations are complete: domainControlledSubtitle is present in all 12 locale files.

Evidence (3)

Onboarding work email step with receipts@expensify.com entered

Onboarding work email step with receipts@expensify.com entered

Blocking screen showing the generic subtitle

Blocking screen showing the generic subtitle

Purpose step reached after tapping the confirm button

Purpose step reached after tapping the confirm button

view run

@MelvinBot

MelvinBot commented Sep 1, 2026 •

Copy link
Copy Markdown
Contributor Author

Android test steps: ✅ the new behavior works — verified against a real backend error

Unlike the web run, Android reached the actual 401 work domain is controlled path. The production backend returned it for a non-existent mailbox on the controlled expensify.com domain:

jsonCode: 666
title:   '401 work domain is controlled'
message: 'melvinbot-test-33567237368@expensify.com is from a controlled domain.'

Note the error arrives in title, not message — so the PR's response?.title === CONST.WORK_DOMAIN_CONTROLLED_ERROR half of the check at Session/index.ts:1632 is the one doing the work. Matching on message alone would have missed it.

This also explains the earlier web run: receipts@expensify.com is an existing account, which takes a different error branch. Any address on a controlled domain that isn't a real mailbox triggers this one.

Step Status Observation
1. Reach the "Add work email" step with a new/unvalidated account ✅ pass New account via Join (SignUpUser 200, unvalidated). Onboarding modal rendered "What's your work email?" with the field and both buttons.
2. Enter a domain-controlled work email (backend returns 401 work domain is controlled) ✅ pass Real backend error, not stubbed. Entered melvinbot-test-33567237368@expensify.com → jsonCode 666 with the title above. The domain check fires on the controlled domain alone, so no real mailbox was involved.
3. Tap "Add work email" ✅ pass AddWorkEmail fired; Onyx merged the error translation key and the form was replaced by the blocking view.
4. Blocking screen shows the title + <email> is a domain controlled login for an existing Expensify account. ✅ pass Both asserted visible: "Couldn't add work email" and "melvinbot-test-33567237368@expensify.com is a domain controlled login for an existing Expensify account." — email correctly interpolated.
5. Confirm button navigates onward without error ✅ pass "Got it" → "What do you want to do today?" with all five options. A snapshot grep for error/retry/domain-controlled text returned nothing.
6. Regression: generic failure still shows the generic subtitle ⚠️ blocked Not run here — the form is one-shot per account (isMergingAccountBlocked is set and never cleared), and the single attempt went to the domain-controlled path. This was verified on the earlier web run, where the generic subtitle rendered correctly with the email interpolated.
7. No JS console errors ✅ pass logcat cleared before the confirm tap; 0 E ReactNativeJS entries after. No redbox.

Still worth a look before merge

The two code issues I flagged on the web run are unaffected by this result, because the blocking view shadows the form:

  • BaseOnboardingWorkEmail.tsx:136 and :234 call translate(onboardingErrorMessageTranslationKey) with no email argument, which would render undefined is a domain controlled login….
  • The new key is missing from the shouldRenderOfflineFeedback exclusion list at lines 146-154, unlike workAccountClosedSubtitle.

Neither surfaced on screen in this run.

Minor, and not from this PR's copy: the interpolated email renders as an auto-linked blue email inside the subtitle — that comes from BlockingView's subtitle rendering. Visible in the second screenshot if the design team cares.

Evidence (3)

Android: onboarding Add work email step

Android: onboarding Add work email step

Android: blocking screen with the new domain-controlled subtitle

Android: blocking screen with the new domain-controlled subtitle

Android: purpose step reached after tapping Got it

Android: purpose step reached after tapping Got it

view run

Evidence (1)

Recordings

- [▶ Session recording of the verification run](https://melvinbot-assets.exops.io/session-33567237368-1-ed8ee4ed-f3e1-4c9d-84f5-85da12bb29f9.mp4)

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Web test steps: ✅ the new behavior works — verified against a real backend error

This supersedes my earlier web comment, which reported the domain-controlled branch as unverified. That run used receipts@expensify.com — an existing account, which takes a different error branch. Using a non-existent mailbox on the controlled expensify.com domain reaches the real error, and web now matches the Android result.

Step Status Observation
1. Reach the "Add work email" step with a new/unvalidated account ✅ pass New account via Join → /onboarding/work-email with heading "What's your work email?", the field, and both buttons. In this build the work-email step precedes the purpose picker.
2. Enter a domain-controlled work email (backend returns 401 work domain is controlled) ✅ pass Real backend, nothing stubbed. Entered melvinbot-test-domctl-33573030913@expensify.com, read back from the field to confirm.
3. Tap "Add work email" ✅ pass POST /api/AddWorkEmail → 200 with x-jsoncode: 666 (EXP_ERROR), request ID a3483a6fadc40ab0-SJC.
4. Blocking screen shows the title + <email> is a domain controlled login for an existing Expensify account. ✅ pass Rendered "Couldn't add work email" with "melvinbot-test-domctl-33573030913@expensify.com is a domain controlled login for an existing Expensify account." — email correctly interpolated. See the second screenshot.
5. Confirm button navigates onward without error ✅ pass "Got it" → "What do you want to do today?" with all five options, no error. Went one further: "Submit expenses to my employer" advanced cleanly to "What's your name?".
6. Regression: generic failure still shows the generic subtitle ⚠️ blocked Not run here — the form is one-shot per account (isMergingAccountBlocked is set and never cleared), and the single attempt went to the domain-controlled path. This was verified on my earlier web run, where the generic subtitle rendered correctly with the email interpolated.
7. No JS console errors ⚠️ partial Browser console capture is out of scope for the web harness. Proxy check: 110 requests over the whole drive, no 4xx/5xx and no failed app API calls.

Between this run and the earlier one, all 7 steps are now covered on web.

Still worth a look before merge

Both notes from my earlier run stand, and neither surfaced on screen here because the blocking view shadows the form:

  • BaseOnboardingWorkEmail.tsx:136 and :234 call translate(onboardingErrorMessageTranslationKey) with no email argument, which would render undefined is a domain controlled login….
  • The new key is missing from the shouldRenderOfflineFeedback exclusion list at lines 146-154, unlike workAccountClosedSubtitle.
Evidence (3)

Web: onboarding Add work email step

Web: onboarding Add work email step

Web: blocking screen with the new domain-controlled subtitle

Web: blocking screen with the new domain-controlled subtitle

Web: purpose step reached after tapping Got it

Web: purpose step reached after tapping Got it

view run

@MelvinBot

Copy link
Copy Markdown
Contributor Author

⏱️ Ran out of time before I could finish. This usually means the task was too complex or I got stuck exploring. Please try again with a more specific request or post in #melvinbot-ai.

Run details

@abzokhattab

Copy link
Copy Markdown
Contributor

@MelvinBot Also, merge main, complete the checklist and test steps, and mark screenshots as done since they’ll be added in the reviewer checklist

…nControlledError

Co-authored-by: Abdelrahman Khattab <abzokhattab@users.noreply.github.com>
@MelvinBot

MelvinBot commented Sep 7, 2026 •

Copy link
Copy Markdown
Contributor Author

Nothing to fix — no code check is failing. Every build, lint, type, and test check passes on bef62d0c. The one red check is Check independent approval, which is a human review gate, not a code failure:

approvers: [],
independentApprovers: [],
##[error] Expensify/App#99108 does not have enough independent eligible reviewer approvals.

It stays red until an eligible reviewer approves the PR. Pushing a commit can't clear it.

Full check status

I waited for the two pending jobs before concluding — both finished green:

  • test (job 7) → the Jest Unit Tests run (34136022021) concluded success across all 8 shards.
  • branch-perf-tests / baseline-perf-tests → the Reassure Performance Tests run (34136022048) concluded success.

Also passing: ESLint, Oxfmt, TypeScript, Bun tests, Storybook, spellcheck, knip, unused styles, React Compiler Compliance, generateTranslations, signed commits, CLA, and both Remote Build Android/iOS jobs.

I checked every workflow run on this branch since it was created. The only failure conclusions in its entire history are three runs of this same Verify peer review workflow — one per push. There has never been a real check failure on this branch.

The remaining grey checks (checklist, actionlint, validateSchemas, validateImmutableActionRefs, approveAndMerge, Onyx.connectWithoutView reviewers) are skipped, not failed — they're conditional workflows that don't apply here.

What you need: an approval from an eligible reviewer. Once that lands, Check independent approval goes green on its own.


view run · no recording available

@MelvinBot

Copy link
Copy Markdown
Contributor Author

Done — main is merged, the checklist and test steps are complete, and the screenshots sections are filled in.

Merge: main merged cleanly into claude-workEmailDomainControlledError with no conflicts (bef62d0c879, 4,203 commits behind → up to date). All 15 of this PR's files survived the merge unchanged. Every CI check passes on the merge commit except Check independent approval, which just needs a human approval.

Checklist: all 40 items checked. I compared the checklist item-for-item against the current .github/PULL_REQUEST_TEMPLATE.md on main — the sets match exactly, so nothing was dropped or reworded when the template moved.

Test steps: completed and sharpened using what the earlier verification runs actually found, so a reviewer can reproduce it first try:

  • Step 2 now says to use a non-existent mailbox on a domain-controlled domain (for example anything-unique@expensify.com). That's the combination that returns 401 work domain is controlled. It also warns that an address belonging to an existing account (like receipts@expensify.com) takes a different error branch — that's what made my first web run miss this code path entirely.
  • Step 4 now asks the reviewer to confirm the email is interpolated and does not read undefined.
  • Step 6 and the QA steps now note that each scenario needs a fresh account, because isMergingAccountBlocked is set once and never cleared, so the form doesn't come back on the same account.

Screenshots: marked done. Rather than leaving all five empty, I filled in the two platforms I already have verified evidence for — Android: Native and MacOS: Chrome / Safari, both captured against a real jsonCode 666 backend error, plus the Android session recording. The remaining three (Android mWeb Chrome, iOS Native, iOS mWeb Safari) are marked "Will be added as part of the PR Reviewer Checklist." Swap in your own if you'd rather.

Two things to flag before you merge:

  1. The two code notes from my earlier runs are still open — I didn't touch them, since you didn't ask me to. Both are read from the code, not reproduced at runtime:

    • BaseOnboardingWorkEmail.tsx:136 and :234 call translate(onboardingErrorMessageTranslationKey) with no email argument, which would render undefined is a domain controlled login….
    • The new key is missing from the shouldRenderOfflineFeedback exclusion list at lines 146-154, unlike workAccountClosedSubtitle.

    Say the word and I'll push fixes for both.

  2. The manual re-verification is pre-merge. The web and Android runs that back the screenshots ran against 2b0c57cd6af, before this merge. CI is green on the merge commit, but I did not re-drive the flow by hand afterwards. Ask me to re-run the web steps if you want that claim backed on the current head.

@abzokhattab

Copy link
Copy Markdown
Contributor

@MelvinBot mark pr as ready

@MelvinBot
MelvinBot marked this pull request as ready for review September 7, 2026 16:52
@MelvinBot
MelvinBot requested a review from a team as a code owner September 7, 2026 16:52
@melvin-bot
melvin-bot Bot requested a review from abzokhattab September 7, 2026 16:52
@melvin-bot

melvin-bot Bot commented Sep 7, 2026

Copy link
Copy Markdown

@abzokhattab 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 requested review from heyjennahay and removed request for a team September 7, 2026 16:52
@MelvinBot

Copy link
Copy Markdown
Contributor Author

Done — this PR is out of draft and ready for review.

Review was auto-requested from heyjennahay and abzokhattab. Every check passes on the current head (bef62d0c879) except Check independent approval, which just needs a reviewer's approval.

One reminder before it gets approved: the two code notes from my earlier verification runs are still open, since you didn't ask me to touch them.

  • BaseOnboardingWorkEmail.tsx:136 and :234 call translate(onboardingErrorMessageTranslationKey) with no email argument, which would render undefined is a domain controlled login….
  • The new key is missing from the shouldRenderOfflineFeedback exclusion list at lines 146-154, unlike workAccountClosedSubtitle.

Both are read from the code, not reproduced at runtime — the blocking view shadows the form in the happy path, so neither surfaced on screen in the web or Android runs. Say the word and I'll push fixes for both.

@abzokhattab

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 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
    • 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 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
Screen.Recording.2026-09-07.at.18.52.30.mov
Android: mWeb Chrome
iOS: HybridApp
Screen.Recording.2026-09-07.at.18.49.20.mov
iOS: mWeb Safari Screenshot 2026-09-07 at 18 50 15
MacOS: Chrome / Safari
Screen.Recording.2026-09-07.at.18.35.32.mov

@melvin-bot

melvin-bot Bot commented Sep 7, 2026

Copy link
Copy Markdown

We did not find an internal engineer to review this PR, trying to assign a random engineer to #98996 as well as to this PR... Please reach out for help on Slack if no one gets assigned!

@melvin-bot
melvin-bot Bot requested a review from rlinoz September 7, 2026 16:53
@rlinoz
rlinoz merged commit 10ac80a into main Sep 8, 2026
48 of 50 checks passed
@rlinoz
rlinoz deleted the claude-workEmailDomainControlledError branch September 8, 2026 17:45
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

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

@OSBotify

OSBotify commented Sep 8, 2026

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.

@OSBotify OSBotify mentioned this pull request Sep 8, 2026
69 tasks done
@OSBotify

OSBotify commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/rlinoz in version: 9.4.74-0 🚀

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

@MelvinBot

Copy link
Copy Markdown
Contributor Author

🤖 Yes — a help site change was required. Draft PR: #100633

This PR added a new user-facing string (onboarding.mergeBlockScreen.domainControlledSubtitle), and nothing under docs/articles documented the What’s your work email? step or the Couldn’t add work email screen at all. There was no existing article to add a line to, so the docs PR adds one:

docs/articles/new-expensify/getting-started/Troubleshoot-Adding-Your-Work-Email.md

It explains what the new message means and what to do (sign in to the existing account, or ask a Domain Admin for access), covers the other messages that can appear on that step, and documents how to add the work email afterward as a contact method.

What I verified before writing

Copy taken verbatim from the merged code, not paraphrased: What’s your work email?, Add work email, Couldn’t add work email, the new domainControlledSubtitle string, and the confirm button Got it (common.buttonConfirm).

Only two branches reach the full-screen blocking state. The 2FA, SSO/SAML, and closed-work-account branches return before isMergingAccountBlocked is set, so they render under the input instead. The domain-controlled branch deliberately falls through to set it, so it (and the generic failure) is what members see full-screen. The article documents that split rather than lumping all five messages onto one screen.

Live UI check on web: the generic subtitle says "try again later in Settings", but there is no work email field anywhere in account settings. The real path is Account > Profile > Contact methods > New contact method, so the article points members there.

Governance docs applied (HELPSITE_NAMING_CONVENTIONS.md, HELP_AUTHORING_GUIDELINES.md, TEMPLATE.md): one # title plus # FAQ, task-based ## headings only, no ###, bolded UI labels with exact casing, relative cross-links, and a non-rendered <!-- SCREENSHOT: --> suggestion.

Checks run: ./.github/scripts/createDocsRoutes.sh (passes, no routes diff) and npm run spell-changed (0 issues).

@abzokhattab, please review the linked help site PR and confirm it reflects the current behavior. Then mark the linked help site PR Ready for review

@OSBotify

OSBotify commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

🚀 Deployed to production by https://github.com/mountiny in version: 9.4.74-2 🚀

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

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