diff --git a/src/components/Navigation/NavigationTabBar/WorkspacesTabButton.tsx b/src/components/Navigation/NavigationTabBar/WorkspacesTabButton.tsx index aa0ee044f14e7..3e2ddee9d58c0 100644 --- a/src/components/Navigation/NavigationTabBar/WorkspacesTabButton.tsx +++ b/src/components/Navigation/NavigationTabBar/WorkspacesTabButton.tsx @@ -57,9 +57,10 @@ function WorkspacesTabButton({selectedTab, isWideLayout}: WorkspacesTabButtonPro return domains?.[`${ONYXKEYS.COLLECTION.DOMAIN}${paramsDomainAccountID}`]; }; const [lastViewedDomain] = useOnyx(ONYXKEYS.COLLECTION.DOMAIN, {selector: lastViewedDomainSelector}, [navigationState]); + const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); const navigateToWorkspaces = () => { - navigateToWorkspacesPage({shouldUseNarrowLayout, currentUserLogin, policy: lastViewedPolicy, domain: lastViewedDomain}); + navigateToWorkspacesPage({shouldUseNarrowLayout, currentUserLogin, policy: lastViewedPolicy, domain: lastViewedDomain, activePolicyID}); }; const workspacesAccessibilityState = {selected: selectedTab === NAVIGATION_TABS.WORKSPACES}; diff --git a/src/hooks/useSearchTypeMenuSections.ts b/src/hooks/useSearchTypeMenuSections.ts index 1116a0fa15b75..b07da374cbab6 100644 --- a/src/hooks/useSearchTypeMenuSections.ts +++ b/src/hooks/useSearchTypeMenuSections.ts @@ -65,6 +65,7 @@ const useSearchTypeMenuSections = (queryParams?: UseSearchTypeMenuSectionsParams const [savedSearches] = useOnyx(ONYXKEYS.SAVED_SEARCHES); const shouldRedirectToExpensifyClassic = useMemo(() => areAllGroupPoliciesExpenseChatDisabled(allPolicies ?? {}), [allPolicies]); const [draftTransactionIDs] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_DRAFT, {selector: validTransactionDraftIDsSelector}); + const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); const [pendingReportCreation, setPendingReportCreation] = useState<{policyID: string; policyName?: string; onConfirm: (shouldDismissEmptyReportsConfirmation: boolean) => void} | null>( null, ); @@ -113,6 +114,7 @@ const useSearchTypeMenuSections = (queryParams?: UseSearchTypeMenuSectionsParams defaultExpensifyCard, shouldRedirectToExpensifyClassic, draftTransactionIDs, + activePolicyID, }), [ currentUserLoginAndAccountID?.email, @@ -126,6 +128,7 @@ const useSearchTypeMenuSections = (queryParams?: UseSearchTypeMenuSectionsParams shouldRedirectToExpensifyClassic, icons, draftTransactionIDs, + activePolicyID, ], ); diff --git a/src/libs/Navigation/helpers/navigateToWorkspacesPage.ts b/src/libs/Navigation/helpers/navigateToWorkspacesPage.ts index df07d7afcc293..82d065139df46 100644 --- a/src/libs/Navigation/helpers/navigateToWorkspacesPage.ts +++ b/src/libs/Navigation/helpers/navigateToWorkspacesPage.ts @@ -1,3 +1,4 @@ +import type {OnyxEntry} from 'react-native-onyx'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import Log from '@libs/Log'; import {getPreservedNavigatorState} from '@libs/Navigation/AppNavigator/createSplitNavigator/usePreserveNavigatorState'; @@ -17,6 +18,7 @@ type Params = { shouldUseNarrowLayout: boolean; policy?: Policy; domain?: Domain; + activePolicyID: OnyxEntry; }; // Gets the latest workspace navigation state, restoring from session or preserved state if needed. @@ -49,7 +51,7 @@ const getWorkspaceNavigationRouteState = () => { }; // Navigates to the appropriate workspace tab or workspace list page. -const navigateToWorkspacesPage = ({currentUserLogin, shouldUseNarrowLayout, policy, domain}: Params) => { +const navigateToWorkspacesPage = ({currentUserLogin, shouldUseNarrowLayout, policy, domain, activePolicyID}: Params) => { const {lastWorkspacesTabNavigatorRoute, topmostFullScreenRoute} = getWorkspaceNavigationRouteState(); if (!topmostFullScreenRoute || topmostFullScreenRoute.name === SCREENS.WORKSPACES_LIST) { @@ -107,7 +109,7 @@ const navigateToWorkspacesPage = ({currentUserLogin, shouldUseNarrowLayout, poli // Fallback: any other state, go to the list. Navigation.navigate(ROUTES.WORKSPACES_LIST.route); - }); + }, activePolicyID); }; export default navigateToWorkspacesPage; diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 31907d214b8e0..7d48d92aa048c 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -3671,6 +3671,7 @@ type TypeMenuSectionsParams = { defaultExpensifyCard: CardFeedForDisplay | undefined; shouldRedirectToExpensifyClassic: boolean; draftTransactionIDs: string[] | undefined; + activePolicyID: OnyxEntry; }; function createTypeMenuSections(params: TypeMenuSectionsParams): SearchTypeMenuSection[] { @@ -3686,6 +3687,7 @@ function createTypeMenuSections(params: TypeMenuSectionsParams): SearchTypeMenuS defaultExpensifyCard, shouldRedirectToExpensifyClassic, draftTransactionIDs, + activePolicyID, } = params; const typeMenuSections: SearchTypeMenuSection[] = []; @@ -3740,7 +3742,7 @@ function createTypeMenuSections(params: TypeMenuSectionsParams): SearchTypeMenuS } startMoneyRequest(CONST.IOU.TYPE.CREATE, generateReportID(), draftTransactionIDs, CONST.IOU.REQUEST_TYPE.SCAN); - }); + }, activePolicyID); }, }, ] diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index 7ebdac9a989fc..8cb4cd664c64c 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -121,11 +121,11 @@ Onyx.connect({ callback: (value) => (stashedCredentials = value ?? {}), }); -let activePolicyID: OnyxEntry; +let deprecatedActivePolicyID: OnyxEntry; Onyx.connect({ key: ONYXKEYS.NVP_ACTIVE_POLICY_ID, callback: (newActivePolicyID) => { - activePolicyID = newActivePolicyID; + deprecatedActivePolicyID = newActivePolicyID; }, }); @@ -322,7 +322,13 @@ const KEYS_TO_PRESERVE_SUPPORTAL = [ ONYXKEYS.IS_USING_IMPORTED_STATE, ]; -function signOutAndRedirectToSignIn(shouldResetToHome?: boolean, shouldStashSession?: boolean, shouldSignOutFromOldDot = true, shouldForceUseStashedSession?: boolean) { +function signOutAndRedirectToSignIn( + shouldResetToHome?: boolean, + shouldStashSession?: boolean, + shouldSignOutFromOldDot = true, + shouldForceUseStashedSession?: boolean, + activePolicyID = deprecatedActivePolicyID, +) { Log.info('Redirecting to Sign In because signOut() was called'); hideContextMenu(false); diff --git a/src/libs/interceptAnonymousUser.ts b/src/libs/interceptAnonymousUser.ts index d4e40cf447796..8a379438b3c33 100644 --- a/src/libs/interceptAnonymousUser.ts +++ b/src/libs/interceptAnonymousUser.ts @@ -1,13 +1,13 @@ -import * as Session from './actions/Session'; +import type {OnyxEntry} from 'react-native-onyx'; +import {isAnonymousUser, signOutAndRedirectToSignIn} from './actions/Session'; /** * Checks if user is anonymous. If true, shows the sign in modal, else, * executes the callback. */ -const interceptAnonymousUser = (callback: () => void) => { - const isAnonymousUser = Session.isAnonymousUser(); - if (isAnonymousUser) { - Session.signOutAndRedirectToSignIn(); +const interceptAnonymousUser = (callback: () => void, activePolicyID?: OnyxEntry) => { + if (isAnonymousUser()) { + signOutAndRedirectToSignIn(undefined, undefined, undefined, undefined, activePolicyID); } else { callback(); } diff --git a/tests/unit/Search/SearchUIUtilsTest.ts b/tests/unit/Search/SearchUIUtilsTest.ts index 91e1f4223c5f1..b3ef91d4e680b 100644 --- a/tests/unit/Search/SearchUIUtilsTest.ts +++ b/tests/unit/Search/SearchUIUtilsTest.ts @@ -25,6 +25,7 @@ import type { TransactionYearGroupListItemType, } from '@components/SelectionListWithSections/types'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import Navigation from '@navigation/Navigation'; // eslint-disable-next-line no-restricted-syntax import type * as ReportUserActions from '@userActions/Report'; @@ -62,6 +63,7 @@ jest.mock('@userActions/Search', () => ({ setOptimisticDataForTransactionThreadPreview: jest.fn(), })); jest.mock('@hooks/useCardFeedsForDisplay', () => jest.fn(() => ({defaultCardFeed: null, cardFeedsByPolicy: {}}))); +jest.mock('@libs/interceptAnonymousUser'); const adminAccountID = 18439984; const adminEmail = 'admin@policy.com'; @@ -5270,6 +5272,7 @@ describe('SearchUIUtils', () => { defaultExpensifyCard: undefined, shouldRedirectToExpensifyClassic: false, draftTransactionIDs: [], + activePolicyID: '', }) .map((section) => section.menuItems) .flat(); @@ -5361,6 +5364,7 @@ describe('SearchUIUtils', () => { defaultExpensifyCard: undefined, shouldRedirectToExpensifyClassic: false, draftTransactionIDs: [], + activePolicyID: '', }); const todoSection = sections.find((section) => section.translationPath === 'common.todo'); @@ -5424,6 +5428,7 @@ describe('SearchUIUtils', () => { defaultExpensifyCard: undefined, shouldRedirectToExpensifyClassic: false, draftTransactionIDs: [], + activePolicyID: '', }); const monthlyAccrualSection = sections.find((section) => section.translationPath === 'search.monthlyAccrual'); @@ -5473,6 +5478,7 @@ describe('SearchUIUtils', () => { defaultExpensifyCard: undefined, shouldRedirectToExpensifyClassic: false, draftTransactionIDs: [], + activePolicyID: '', }); const savedSection = sections.find((section) => section.translationPath === 'search.savedSearchesMenuItemTitle'); @@ -5495,6 +5501,7 @@ describe('SearchUIUtils', () => { defaultExpensifyCard: undefined, shouldRedirectToExpensifyClassic: false, draftTransactionIDs: [], + activePolicyID: '', }); const savedSection = sections.find((section) => section.translationPath === 'search.savedSearchesMenuItemTitle'); @@ -5524,6 +5531,7 @@ describe('SearchUIUtils', () => { defaultExpensifyCard: undefined, shouldRedirectToExpensifyClassic: false, draftTransactionIDs: [], + activePolicyID: '', }); const savedSection = sections.find((section) => section.translationPath === 'search.savedSearchesMenuItemTitle'); @@ -5553,6 +5561,7 @@ describe('SearchUIUtils', () => { defaultExpensifyCard: undefined, shouldRedirectToExpensifyClassic: false, draftTransactionIDs: [], + activePolicyID: '', }); const savedSection = sections.find((section) => section.translationPath === 'search.savedSearchesMenuItemTitle'); @@ -5586,6 +5595,7 @@ describe('SearchUIUtils', () => { defaultExpensifyCard: undefined, shouldRedirectToExpensifyClassic: false, draftTransactionIDs: [], + activePolicyID: '', }); const todoSection = sections.find((section) => section.translationPath === 'common.todo'); @@ -5619,6 +5629,7 @@ describe('SearchUIUtils', () => { defaultExpensifyCard: undefined, shouldRedirectToExpensifyClassic: false, draftTransactionIDs: [], + activePolicyID: '', }); const monthlyAccrualSection = sections.find((section) => section.translationPath === 'search.monthlyAccrual'); @@ -5664,6 +5675,7 @@ describe('SearchUIUtils', () => { defaultExpensifyCard: undefined, shouldRedirectToExpensifyClassic: false, draftTransactionIDs: [], + activePolicyID: '', }); const reconciliationSection = sections.find((section) => section.translationPath === 'search.reconciliation'); @@ -5703,6 +5715,7 @@ describe('SearchUIUtils', () => { defaultExpensifyCard: undefined, shouldRedirectToExpensifyClassic: false, draftTransactionIDs: [], + activePolicyID: '', }); const reconciliationSection = sections.find((section) => section.translationPath === 'search.reconciliation'); expect(reconciliationSection).toBeDefined(); @@ -5727,6 +5740,7 @@ describe('SearchUIUtils', () => { defaultExpensifyCard: undefined, shouldRedirectToExpensifyClassic: false, draftTransactionIDs: [], + activePolicyID: '', }) .map((section) => section.menuItems) .flat(); @@ -5802,6 +5816,7 @@ describe('SearchUIUtils', () => { defaultExpensifyCard: undefined, shouldRedirectToExpensifyClassic: false, draftTransactionIDs: [], + activePolicyID: '', }); const todoSection = sections.find((section) => section.translationPath === 'common.todo'); expect(todoSection).toBeDefined(); @@ -5816,6 +5831,46 @@ describe('SearchUIUtils', () => { expect(payItem).toBeDefined(); expect(exportItem).toBeDefined(); }); + + it('should pass activePolicyID to interceptAnonymousUser in submit button action', () => { + const mockPolicies: OnyxCollection = { + [`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]: { + id: policyID, + name: 'Test Policy', + type: CONST.POLICY.TYPE.TEAM, + role: CONST.POLICY.ROLE.ADMIN, + owner: adminEmail, + isPolicyExpenseChatEnabled: true, + pendingAction: undefined, + } as OnyxTypes.Policy, + }; + + const testActivePolicyID = 'test-active-policy-456'; + const {result: icons} = renderHook(() => useMemoizedLazyExpensifyIcons(['Document', 'Send', 'ThumbsUp'])); + const sections = SearchUIUtils.createTypeMenuSections({ + icons: icons.current, + currentUserEmail: adminEmail, + currentUserAccountID: adminAccountID, + cardFeedsByPolicy: {}, + defaultCardFeed: undefined, + policies: mockPolicies, + savedSearches: {}, + isOffline: false, + defaultExpensifyCard: undefined, + shouldRedirectToExpensifyClassic: false, + draftTransactionIDs: [], + activePolicyID: testActivePolicyID, + }); + + const todoSection = sections.find((section) => section.translationPath === 'common.todo'); + const submitItem = todoSection?.menuItems.find((item) => item.key === CONST.SEARCH.SEARCH_KEYS.SUBMIT); + const buttonAction = submitItem?.emptyState?.buttons?.[0]?.buttonAction; + + if (buttonAction) { + buttonAction(); + expect(interceptAnonymousUser).toHaveBeenCalledWith(expect.any(Function), testActivePolicyID); + } + }); }); describe('Test isSearchResultsEmpty', () => { diff --git a/tests/unit/interceptAnonymousUserTest.ts b/tests/unit/interceptAnonymousUserTest.ts new file mode 100644 index 0000000000000..27f2dca842d58 --- /dev/null +++ b/tests/unit/interceptAnonymousUserTest.ts @@ -0,0 +1,44 @@ +// eslint-disable-next-line no-restricted-syntax +import * as Session from '@libs/actions/Session'; +import interceptAnonymousUser from '@libs/interceptAnonymousUser'; + +jest.mock('@libs/actions/Session', () => ({ + isAnonymousUser: jest.fn(), + signOutAndRedirectToSignIn: jest.fn(), +})); + +describe('interceptAnonymousUser', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('calls signOutAndRedirectToSignIn with activePolicyID when user is anonymous', () => { + (Session.isAnonymousUser as jest.Mock).mockReturnValue(true); + const callback = jest.fn(); + + interceptAnonymousUser(callback, 'policy-123'); + + expect(Session.signOutAndRedirectToSignIn).toHaveBeenCalledWith(undefined, undefined, undefined, undefined, 'policy-123'); + expect(callback).not.toHaveBeenCalled(); + }); + + it('calls signOutAndRedirectToSignIn with undefined activePolicyID when not provided', () => { + (Session.isAnonymousUser as jest.Mock).mockReturnValue(true); + const callback = jest.fn(); + + interceptAnonymousUser(callback); + + expect(Session.signOutAndRedirectToSignIn).toHaveBeenCalledWith(undefined, undefined, undefined, undefined, undefined); + expect(callback).not.toHaveBeenCalled(); + }); + + it('executes callback and does not sign out when user is not anonymous', () => { + (Session.isAnonymousUser as jest.Mock).mockReturnValue(false); + const callback = jest.fn(); + + interceptAnonymousUser(callback, 'policy-123'); + + expect(callback).toHaveBeenCalledTimes(1); + expect(Session.signOutAndRedirectToSignIn).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/unit/navigateToWorkspacesPageTest.ts b/tests/unit/navigateToWorkspacesPageTest.ts index 00ca113d1b811..7eb24b414d522 100644 --- a/tests/unit/navigateToWorkspacesPageTest.ts +++ b/tests/unit/navigateToWorkspacesPageTest.ts @@ -21,7 +21,7 @@ jest.mock('@libs/interceptAnonymousUser'); const fakePolicyID = '344559B2CCF2B6C1'; const mockPolicy = {...createRandomPolicy(0), id: fakePolicyID}; -const mockParams = {currentUserLogin: 'test@example.com', shouldUseNarrowLayout: false, policy: mockPolicy}; +const mockParams = {currentUserLogin: 'test@example.com', shouldUseNarrowLayout: false, policy: mockPolicy, activePolicyID: 'test-active-policy-id'}; describe('navigateToWorkspacesPage', () => { beforeEach(() => { @@ -160,6 +160,21 @@ describe('navigateToWorkspacesPage', () => { expect(Navigation.navigate).toHaveBeenCalledWith(ROUTES.WORKSPACES_LIST.route); }); + it('passes activePolicyID to interceptAnonymousUser', () => { + (navigationRef.getRootState as jest.Mock).mockReturnValue({ + routes: [{name: NAVIGATORS.REPORTS_SPLIT_NAVIGATOR}], + }); + + (lastVisitedTabPathUtils.getWorkspacesTabStateFromSessionStorage as jest.Mock).mockReturnValue({ + routes: [{name: NAVIGATORS.REPORTS_SPLIT_NAVIGATOR}], + }); + + (interceptAnonymousUser as jest.Mock).mockImplementation(() => {}); + navigateToWorkspacesPage({...mockParams, activePolicyID: 'my-policy-123'}); + + expect(interceptAnonymousUser).toHaveBeenCalledWith(expect.any(Function), 'my-policy-123'); + }); + it('navigates to WORKSPACES_LIST if policyID is missing', () => { (navigationRef.getRootState as jest.Mock).mockReturnValue({ routes: [{name: NAVIGATORS.REPORTS_SPLIT_NAVIGATOR}],