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:
- Sign in to New Expensify with an account whose primary login is a work email (not a phone number, not a public domain).
- 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.)
- Open a workspace → Travel.
- 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:
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 Owner
Current Issue Owner: @daledah
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
mainas of9956186Reproducible 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:
account.primaryLoginis now absent from Onyx — confirm in the Onyx debug view.)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.phoneErrorstring, 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:
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
BookTravelButtonis dead code.src/components/BookTravelButton.tsx:71and:78:primaryLoginhas already been defaulted to''. Because''is not nullish,?? sessionEmail ?? ''never evaluates — the intended fallback to the session email is unreachable.2.
account.primaryLoginis not always present in Onyx.Account.primaryLoginis correctly typed optional insrc/types/onyx/Account.ts. Within App it is written only by sign-in responses, bysetContactMethodAsDefaultinsrc/libs/actions/User.ts, and by the HybridApp delegate path insrc/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:139reads'', takes the!primaryContactMethodbranch, and renders the error.Str.isSMSLogin('')isfalse, 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:Several card and validate-code pages already use it, so this makes travel consistent with the rest of the app and lets the two
useOnyxcalls inBookTravelButtonbe dropped.Two follow-ups worth doing in the same pass:
travel.phoneErrorsays "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.primaryLoginin 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.primaryLogindirectly 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 onprimaryLoginand the backend change becomes the required path.Slack thread: https://expensify.slack.com/archives/C0BCEQ2UNSG/p1788992380267439
View all open jobs on GitHub
Issue Owner
Current Issue Owner: @daledah