From de99bd2ad5c4f8eddd57f2e4a4888cee23963338 Mon Sep 17 00:00:00 2001 From: "daledah (via MelvinBot)" Date: Fri, 11 Sep 2026 06:58:48 +0000 Subject: [PATCH 1/3] Fall back to the session email when account.primaryLogin is missing in BookTravelButton Co-authored-by: daledah --- src/components/BookTravelButton.tsx | 7 ++---- .../unit/components/BookTravelButtonTest.tsx | 24 +++++++++++++++++++ 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/components/BookTravelButton.tsx b/src/components/BookTravelButton.tsx index 29cdf852d7df..b08c26d2eb16 100644 --- a/src/components/BookTravelButton.tsx +++ b/src/components/BookTravelButton.tsx @@ -7,6 +7,7 @@ import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import usePermissions from '@hooks/usePermissions'; import usePolicy from '@hooks/usePolicy'; +import usePrimaryContactMethod from '@hooks/usePrimaryContactMethod'; import useScreenBoundDynamicRoute from '@hooks/useScreenBoundDynamicRoute'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -28,7 +29,6 @@ import type WithSentryLabel from '@src/types/utils/SentryLabel'; import type {ReactElement} from 'react'; -import {emailSelector} from '@selectors/Session'; import {Str} from 'expensify-common'; import React, {useEffect, useState} from 'react'; @@ -67,15 +67,12 @@ function BookTravelButton({ const illustrations = useMemoizedLazyIllustrations(['RocketDude']); const {translate} = useLocalize(); const {environmentURL} = useEnvironment(); - const [account] = useOnyx(ONYXKEYS.ACCOUNT); - const primaryLogin = account?.primaryLogin ?? ''; const policy = usePolicy(activePolicyID); const blockIfDefaultWorkspaceLacksTravel = useDefaultWorkspaceTravelGuard(); const [errorMessage, setErrorMessage] = useState(''); const [travelSettings] = useOnyx(ONYXKEYS.NVP_TRAVEL_SETTINGS); - const [sessionEmail] = useOnyx(ONYXKEYS.SESSION, {selector: emailSelector}); - const primaryContactMethod = primaryLogin ?? sessionEmail ?? ''; + const primaryContactMethod = usePrimaryContactMethod(); const {isBetaEnabled} = usePermissions(); const {showConfirmModal} = useConfirmModal(); const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); diff --git a/tests/unit/components/BookTravelButtonTest.tsx b/tests/unit/components/BookTravelButtonTest.tsx index 6995535406e0..8c3e12ae7822 100644 --- a/tests/unit/components/BookTravelButtonTest.tsx +++ b/tests/unit/components/BookTravelButtonTest.tsx @@ -328,6 +328,30 @@ describe('BookTravelButton', () => { }); }); + describe('when account.primaryLogin is absent from Onyx (e.g. a session restored from storage after a reload)', () => { + it('falls back to the session email instead of blocking the user with the contact-method error', async () => { + // Given a validated admin whose work email is only known from the session, not from account.primaryLogin + await act(async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, provisionedPolicy); + await Onyx.merge(ONYXKEYS.ACCOUNT, {validated: true}); + await Onyx.merge(ONYXKEYS.SESSION, {email: USER_LOGIN}); + await Onyx.merge(ONYXKEYS.NVP_TRAVEL_SETTINGS, {hasAcceptedTerms: false}); + await Onyx.merge(ONYXKEYS.PRIVATE_PERSONAL_DETAILS, {legalFirstName: 'Test', legalLastName: 'User'}); + await waitForBatchedUpdatesWithAct(); + }); + renderBookTravelButton(); + await waitForBatchedUpdatesWithAct(); + + // When the admin presses the book travel button + fireEvent.press(screen.getByText('Book a trip')); + await waitForBatchedUpdatesWithAct(); + + // Then travel enablement proceeds rather than surfacing the "add a work email" error + expect(Navigation.navigate).toHaveBeenCalledWith(ENABLE_TRAVEL_ROUTE); + expect(screen.queryByText(/add a work email as your primary login/)).toBeNull(); + }); + }); + describe('when the user has a personal-email login', () => { it('shows the public-domain error even when legal details are missing', async () => { // Given a user logged in with a public-domain email and no legal name set yet From b44d4739fb0e287aa0d3ed56ac23aacd326c3117 Mon Sep 17 00:00:00 2001 From: "daledah (via MelvinBot)" Date: Fri, 11 Sep 2026 07:01:11 +0000 Subject: [PATCH 2/3] Remove the blank line left behind by the dropped account useOnyx call Co-authored-by: daledah --- src/components/BookTravelButton.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/BookTravelButton.tsx b/src/components/BookTravelButton.tsx index b08c26d2eb16..600815723502 100644 --- a/src/components/BookTravelButton.tsx +++ b/src/components/BookTravelButton.tsx @@ -67,7 +67,6 @@ function BookTravelButton({ const illustrations = useMemoizedLazyIllustrations(['RocketDude']); const {translate} = useLocalize(); const {environmentURL} = useEnvironment(); - const policy = usePolicy(activePolicyID); const blockIfDefaultWorkspaceLacksTravel = useDefaultWorkspaceTravelGuard(); const [errorMessage, setErrorMessage] = useState(''); From 22fa38d41427c57e3781a24604e5f8a13feb043f Mon Sep 17 00:00:00 2001 From: "daledah (via MelvinBot)" Date: Fri, 11 Sep 2026 09:25:44 +0000 Subject: [PATCH 3/3] Treat an empty primaryLogin as missing in usePrimaryContactMethod account.primaryLogin is sometimes written as an empty string rather than being absent. Because '' is not nullish, ?? never fell through to the session email, so the hook returned '' and BookTravelButton still showed the contact-method error for those users. Use || so an empty primaryLogin falls back to the session email, matching the existing precedent in VerifyAccountPageBase. Co-authored-by: daledah --- src/hooks/usePrimaryContactMethod.ts | 4 +++- .../unit/components/BookTravelButtonTest.tsx | 22 +++++++++++++++++ .../hooks/usePrimaryContactMethod.test.ts | 24 +++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/hooks/usePrimaryContactMethod.ts b/src/hooks/usePrimaryContactMethod.ts index e74503410a6b..47135eb7b7be 100644 --- a/src/hooks/usePrimaryContactMethod.ts +++ b/src/hooks/usePrimaryContactMethod.ts @@ -7,7 +7,9 @@ import useOnyx from './useOnyx'; function usePrimaryContactMethod(): string { const [account] = useOnyx(ONYXKEYS.ACCOUNT); const [sessionEmail] = useOnyx(ONYXKEYS.SESSION, {selector: emailSelector}); - return account?.primaryLogin ?? sessionEmail ?? ''; + // primaryLogin is sometimes stored as an empty string rather than being absent, so treat it as missing and fall back to the session email. + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + return account?.primaryLogin || sessionEmail || ''; } export default usePrimaryContactMethod; diff --git a/tests/unit/components/BookTravelButtonTest.tsx b/tests/unit/components/BookTravelButtonTest.tsx index 8c3e12ae7822..c558c157cc76 100644 --- a/tests/unit/components/BookTravelButtonTest.tsx +++ b/tests/unit/components/BookTravelButtonTest.tsx @@ -350,6 +350,28 @@ describe('BookTravelButton', () => { expect(Navigation.navigate).toHaveBeenCalledWith(ENABLE_TRAVEL_ROUTE); expect(screen.queryByText(/add a work email as your primary login/)).toBeNull(); }); + + it('falls back to the session email when primaryLogin is stored as an empty string', async () => { + // Given a validated admin whose account.primaryLogin is present but empty, a state the app writes in practice + await act(async () => { + await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, provisionedPolicy); + await Onyx.merge(ONYXKEYS.ACCOUNT, {validated: true, primaryLogin: ''}); + await Onyx.merge(ONYXKEYS.SESSION, {email: USER_LOGIN}); + await Onyx.merge(ONYXKEYS.NVP_TRAVEL_SETTINGS, {hasAcceptedTerms: false}); + await Onyx.merge(ONYXKEYS.PRIVATE_PERSONAL_DETAILS, {legalFirstName: 'Test', legalLastName: 'User'}); + await waitForBatchedUpdatesWithAct(); + }); + renderBookTravelButton(); + await waitForBatchedUpdatesWithAct(); + + // When the admin presses the book travel button + fireEvent.press(screen.getByText('Book a trip')); + await waitForBatchedUpdatesWithAct(); + + // Then travel enablement proceeds rather than surfacing the "add a work email" error + expect(Navigation.navigate).toHaveBeenCalledWith(ENABLE_TRAVEL_ROUTE); + expect(screen.queryByText(/add a work email as your primary login/)).toBeNull(); + }); }); describe('when the user has a personal-email login', () => { diff --git a/tests/unit/hooks/usePrimaryContactMethod.test.ts b/tests/unit/hooks/usePrimaryContactMethod.test.ts index e7b82701cd2e..9b9480a74ddd 100644 --- a/tests/unit/hooks/usePrimaryContactMethod.test.ts +++ b/tests/unit/hooks/usePrimaryContactMethod.test.ts @@ -51,6 +51,30 @@ describe('usePrimaryContactMethod', () => { expect(result.current).toBe('session-only@expensify.com'); }); + it('should fall back to session email when primaryLogin is an empty string', async () => { + await act(async () => { + await Onyx.merge(ONYXKEYS.ACCOUNT, {primaryLogin: ''}); + await Onyx.merge(ONYXKEYS.SESSION, {email: 'session-only@expensify.com'}); + await waitForBatchedUpdates(); + }); + + const {result} = renderHook(() => usePrimaryContactMethod()); + + expect(result.current).toBe('session-only@expensify.com'); + }); + + it('should return empty string when primaryLogin is empty and there is no session email', async () => { + await act(async () => { + await Onyx.merge(ONYXKEYS.ACCOUNT, {primaryLogin: ''}); + await Onyx.merge(ONYXKEYS.SESSION, {}); + await waitForBatchedUpdates(); + }); + + const {result} = renderHook(() => usePrimaryContactMethod()); + + expect(result.current).toBe(''); + }); + it('should return empty string when neither primaryLogin nor session email exist', async () => { await act(async () => { await Onyx.merge(ONYXKEYS.ACCOUNT, {});