Skip to content

Travel - "Please add a work email as your primary login to book travel" blocks users who already have a work email #100870

Description

@MelvinBot

If you haven’t already, check out our contributing guidelines for onboarding. To join our Slack channel, fill out this form.


Version Number: Not captured at report time — confirmed present in main as of 9956186
Reproducible in staging?: Yes
Reproducible in production?: Yes
If this was caught during regression testing, add the test name, ID and link from BrowserStack: N/A — reported by a customer and reproduced internally
Email or phone of affected tester (no customers): sheena@expensifail.com
Logs: https://stackoverflow.com/c/expensify/questions/4856
Expensify/Expensify Issue URL: https://github.com/Expensify/Expensify/issues/680367
Issue reported by: Applause / Guide (internal report)
Slack conversation (hyperlinked to channel name): Slack

Action Performed:

  1. Sign in to New Expensify with an account whose primary login is a work email (not a phone number, not a public domain).
  2. Hard-reload the app, so the session is restored from storage rather than from a fresh sign-in response. (account.primaryLogin is now absent from Onyx — confirm in the Onyx debug view.)
  3. Open a workspace → Travel.
  4. Press the travel button ("Get started" / "Book travel").

Expected Result:

Travel enablement proceeds. The user already has a work email as their primary login, so no contact-method error should appear.

Actual Result:

The inline error "Please add a work email as your primary login to book travel." renders below the button and travel enablement is blocked.

The message is misleading: this branch does not check the email domain at all. It is the travel.phoneError string, guarded by a condition that means "primary login is missing or is a phone number".

Workaround:

Yes — the user can enable and use Travel from Expensify Classic, which has no equivalent client-side guard. Reloading the page can also clear it once a code path repopulates account.primaryLogin, which is why the failure looks intermittent. The customer has been informed of the Classic workaround.

Platforms:

Select the officially supported platforms where the issue was reproduced:

  • Android: App
  • Android: mWeb Chrome
  • iOS: App
  • iOS: mWeb Safari
  • iOS: mWeb Chrome
  • Windows: Chrome
  • MacOS: Chrome / Safari

The guard is platform-agnostic client logic, so it is expected to reproduce on every platform. Only web was verified.

Screenshots/Videos


Root cause

Two things combine. The first is entirely in this repo.

1. The fallback in BookTravelButton is dead code.

src/components/BookTravelButton.tsx:71 and :78:

const primaryLogin = account?.primaryLogin ?? '';
// ...
const primaryContactMethod = primaryLogin ?? sessionEmail ?? '';

primaryLogin has already been defaulted to ''. Because '' is not nullish, ?? sessionEmail ?? '' never evaluates — the intended fallback to the session email is unreachable.

2. account.primaryLogin is not always present in Onyx.

Account.primaryLogin is correctly typed optional in src/types/onyx/Account.ts. Within App it is written only by sign-in responses, by setContactMethodAsDefault in src/libs/actions/User.ts, and by the HybridApp delegate path in src/libs/actions/Session/index.ts. The app-init payload does not populate it, so any session that reaches the Travel page without one of those writes — a reload, a session restored from storage, a copilot switch, a new or unvalidated account — has it undefined.

Result: the guard at src/components/BookTravelButton.tsx:139 reads '', takes the !primaryContactMethod branch, and renders the error. Str.isSMSLogin('') is false, so this is unambiguously the empty-value branch, not real SMS-login detection. That explains every symptom: per-session and intermittent, hits users with confirmed work emails, and Classic is unaffected because it has no equivalent guard.

Note the same empty string also flows into the public-domain check at :146, so that branch is evaluating against no data either.

Proposed fix

Replace both lines with the existing hook, usePrimaryContactMethod(), which returns exactly the fallback this code was trying to express:

return account?.primaryLogin ?? sessionEmail ?? '';

Several card and validate-code pages already use it, so this makes travel consistent with the rest of the app and lets the two useOnyx calls in BookTravelButton be dropped.

Two follow-ups worth doing in the same pass:

  • Copy. travel.phoneError says "add a work email as your primary login", but the condition it guards is "primary login is a phone number". Nothing about it checks the domain — the actual public-domain check is a separate branch that navigates to a full error page. The wording should describe the SMS-login case, so this class of bug is not misdiagnosed as a domain problem again.
  • Backend, defense in depth. Returning primaryLogin in the app-init account payload would close the underlying data gap; the session-email fallback is a client-side patch over missing data. This part is tracked internally in Expensify/Expensify#680367 and is out of scope for this issue.

Open question for travel engineering: is there a reason this guard reads account.primaryLogin directly rather than the session email? Spotnana needs a real mailbox for booking confirmations, and the session email should always be one — but if there is a case where the session email is a phone number while the primary login is not, the check must stay on primaryLogin and the backend change becomes the required path.

Slack thread: https://expensify.slack.com/archives/C0BCEQ2UNSG/p1788992380267439

View all open jobs on GitHub

Issue OwnerCurrent Issue Owner: @daledah

Activity

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

Metadata

Metadata

Labels

BugSomething is broken. Auto assigns a BugZero manager.ExternalAdded to denote the issue can be worked on by a contributorHigh PriorityReviewingHas a PR in reviewWeeklyKSv2

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions