diff --git a/src/components/BookTravelButton.tsx b/src/components/BookTravelButton.tsx index 29cdf852d7df..600815723502 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,11 @@ 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/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 6995535406e0..c558c157cc76 100644 --- a/tests/unit/components/BookTravelButtonTest.tsx +++ b/tests/unit/components/BookTravelButtonTest.tsx @@ -328,6 +328,52 @@ 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(); + }); + + 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', () => { 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 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, {});