diff --git a/src/hooks/useAutoCreateSubmitWorkspace.ts b/src/hooks/useAutoCreateSubmitWorkspace.ts index c21193acf5fd..4494c5240a8c 100644 --- a/src/hooks/useAutoCreateSubmitWorkspace.ts +++ b/src/hooks/useAutoCreateSubmitWorkspace.ts @@ -126,7 +126,11 @@ function useAutoCreateSubmitWorkspace() { policyIDForNavigation = existingSubmitPolicyID; } - navigateToSubmitWorkspaceAfterOnboardingWithMicrotaskQueue(policyIDForNavigation, shouldUseNarrowLayout); + if (conciergeReportID) { + navigateToSubmitWorkspaceAfterOnboardingWithMicrotaskQueue(policyIDForNavigation, shouldUseNarrowLayout, conciergeReportID); + } else { + navigateToSubmitWorkspaceAfterOnboardingWithMicrotaskQueue(policyIDForNavigation, shouldUseNarrowLayout); + } }, [ currentUserEmail, @@ -148,6 +152,7 @@ function useAutoCreateSubmitWorkspace() { hasActiveAdminPolicies, shouldUseNarrowLayout, conciergeChat, + conciergeReportID, ], ); diff --git a/src/libs/PendingConciergeDeepLink.ts b/src/libs/PendingConciergeDeepLink.ts new file mode 100644 index 000000000000..94e7592c49d4 --- /dev/null +++ b/src/libs/PendingConciergeDeepLink.ts @@ -0,0 +1,17 @@ +let hasPendingConciergeDeepLink = false; + +function setPendingConciergeDeepLink() { + hasPendingConciergeDeepLink = true; +} + +function consumePendingConciergeDeepLink() { + const shouldNavigateToConcierge = hasPendingConciergeDeepLink; + hasPendingConciergeDeepLink = false; + return shouldNavigateToConcierge; +} + +function clearPendingConciergeDeepLink() { + hasPendingConciergeDeepLink = false; +} + +export {setPendingConciergeDeepLink, consumePendingConciergeDeepLink, clearPendingConciergeDeepLink}; diff --git a/src/libs/actions/Link.ts b/src/libs/actions/Link.ts index 21bd56d06c86..dbb46dbec2f0 100644 --- a/src/libs/actions/Link.ts +++ b/src/libs/actions/Link.ts @@ -16,6 +16,7 @@ import Navigation from '@libs/Navigation/Navigation'; import navigationRef from '@libs/Navigation/navigationRef'; import REPORT_LINK_ROUTE_PARAMS from '@libs/Navigation/reportLinkRouteParams'; import {getIsOffline} from '@libs/NetworkState'; +import {clearPendingConciergeDeepLink, setPendingConciergeDeepLink} from '@libs/PendingConciergeDeepLink'; import {findLastAccessedReport, getReportIDFromLink, getReportOrDraftReport, getRouteFromLink, isMoneyRequestReport} from '@libs/ReportUtils'; import shouldSkipDeepLinkNavigation from '@libs/shouldSkipDeepLinkNavigation'; import {endSpan, getSpan, startSpan} from '@libs/telemetry/activeSpans'; @@ -451,6 +452,12 @@ function openLink(href: string, environmentURL: string, isAttachment = false) { openExternalLink(href); } +function isConciergeRoute(route: string) { + const [routeWithoutParams] = normalizePath(route).split(/[?#]/, 1); + const normalizedRoute = routeWithoutParams.replace(/\/$/, ''); + return normalizedRoute === normalizePath(ROUTES.CONCIERGE); +} + function openReportFromDeepLink( url: string, reports: OnyxCollection, @@ -496,6 +503,14 @@ function openReportFromDeepLink( route = ''; } + if (!isAuthenticated) { + if (isConciergeRoute(route)) { + setPendingConciergeDeepLink(); + } else { + clearPendingConciergeDeepLink(); + } + } + // If we are not authenticated and are navigating to a public screen, we don't want to navigate again to the screen after sign-in/sign-up if (!isAuthenticated && isPublicScreenRoute(route)) { return; diff --git a/src/libs/actions/SignInRedirect.ts b/src/libs/actions/SignInRedirect.ts index 320788daaa06..f7dccaf20d44 100644 --- a/src/libs/actions/SignInRedirect.ts +++ b/src/libs/actions/SignInRedirect.ts @@ -1,6 +1,7 @@ import {getMicroSecondOnyxErrorWithMessage} from '@libs/ErrorUtils'; import {clearSessionStorage} from '@libs/Navigation/helpers/lastVisitedTabPathUtils'; import {getIsOffline} from '@libs/NetworkState'; +import {clearPendingConciergeDeepLink} from '@libs/PendingConciergeDeepLink'; import clearPrefetchOnAppStart from '@libs/Prefetch/clearPrefetchOnAppStart'; import CONFIG from '@src/CONFIG'; @@ -48,6 +49,8 @@ Onyx.connectWithoutView({ }); function clearStorageAndRedirect(errorMessage?: string, isSAMLReauthentication?: boolean): Promise { + clearPendingConciergeDeepLink(); + // Under certain conditions, there are key-values we'd like to keep in storage even when a user is logged out. // We pass these into the clear() method in order to avoid having to reset them on a delayed tick and getting // flashes of unwanted default state. diff --git a/src/libs/navigateAfterOnboarding.ts b/src/libs/navigateAfterOnboarding.ts index 82aa05acc0e3..c2c6c927513a 100644 --- a/src/libs/navigateAfterOnboarding.ts +++ b/src/libs/navigateAfterOnboarding.ts @@ -3,6 +3,7 @@ import {handleRHPVariantNavigation, shouldOpenRHPVariant} from '@components/Side import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {Route} from '@src/ROUTES'; import type {OnboardingRHPVariant, ReportNameValuePairs} from '@src/types/onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; @@ -16,6 +17,7 @@ import isReportTopmostSplitNavigator from './Navigation/helpers/isReportTopmostS import {dismissOnboardingModalBeforeExit} from './Navigation/helpers/OnboardingNavigationUtils'; import shouldOpenOnAdminRoom from './Navigation/helpers/shouldOpenOnAdminRoom'; import Navigation from './Navigation/Navigation'; +import {consumePendingConciergeDeepLink} from './PendingConciergeDeepLink'; import {findLastAccessedReport, isConciergeChatReport, isSelfDM} from './ReportUtils'; import {buildCannedSearchQuery} from './SearchQueryUtils'; @@ -78,13 +80,21 @@ function navigateAfterOnboarding( shouldPreventOpenAdminRoom = false, options?: NavigateAfterOnboardingOptions, ) { + const navigationOptions = options?.afterTransition ? {afterTransition: options.afterTransition} : undefined; + // A pending /concierge signup deep link should win before onboarding variants or workspace/admin fallbacks choose their standard destinations. + if (consumePendingConciergeDeepLink()) { + setDisableDismissOnEscape(false); + const pendingConciergeRoute: Route = conciergeReportID ? ROUTES.REPORT_WITH_ID.getRoute(conciergeReportID) : ROUTES.CONCIERGE; + Navigation.navigate(pendingConciergeRoute, navigationOptions); + return; + } + setDisableDismissOnEscape(false); // On mobile (small screen), Track workspace admins with the trackExpensesWithConcierge variant // should navigate directly to the Concierge DM (which contains onboarding tasks). // This check is outside shouldOpenRHPVariant because that function returns false on native // (Side Panel doesn't exist on native), but we still need to navigate to Concierge on mobile. - const navigationOptions = options?.afterTransition ? {afterTransition: options.afterTransition} : undefined; const variantOverride = options?.variantOverride; const variant = variantOverride ?? onboardingRHPVariant; if (isSmallScreenWidth && variant === CONST.ONBOARDING_RHP_VARIANT.TRACK_EXPENSES_WITH_CONCIERGE) { @@ -143,9 +153,15 @@ function navigateAfterOnboardingWithMicrotaskQueue( * After creating or joining a Submit workspace during onboarding, navigate to Spend > Expenses * with the side panel open so the #admins room is visible in Concierge Anywhere. */ -function navigateToSubmitWorkspaceAfterOnboarding(policyID?: string, shouldUseNarrowLayout = false) { +function navigateToSubmitWorkspaceAfterOnboarding(policyID?: string, shouldUseNarrowLayout = false, conciergeReportID?: string) { setDisableDismissOnEscape(false); + if (consumePendingConciergeDeepLink()) { + const pendingConciergeRoute: Route = conciergeReportID ? ROUTES.REPORT_WITH_ID.getRoute(conciergeReportID) : ROUTES.CONCIERGE; + Navigation.navigate(pendingConciergeRoute); + return; + } + if (!policyID) { Navigation.navigate(ROUTES.HOME); return; @@ -156,10 +172,10 @@ function navigateToSubmitWorkspaceAfterOnboarding(policyID?: string, shouldUseNa SidePanelActions.openSidePanel(!shouldUseNarrowLayout); } -function navigateToSubmitWorkspaceAfterOnboardingWithMicrotaskQueue(policyID?: string, shouldUseNarrowLayout = false) { +function navigateToSubmitWorkspaceAfterOnboardingWithMicrotaskQueue(policyID?: string, shouldUseNarrowLayout = false, conciergeReportID?: string) { dismissOnboardingModalBeforeExit(); Navigation.setNavigationActionToMicrotaskQueue(() => { - navigateToSubmitWorkspaceAfterOnboarding(policyID, shouldUseNarrowLayout); + navigateToSubmitWorkspaceAfterOnboarding(policyID, shouldUseNarrowLayout, conciergeReportID); }); } diff --git a/src/pages/OnboardingWorkspaces/BaseOnboardingWorkspaces.tsx b/src/pages/OnboardingWorkspaces/BaseOnboardingWorkspaces.tsx index e4e0c0b32bf9..758c3771badb 100644 --- a/src/pages/OnboardingWorkspaces/BaseOnboardingWorkspaces.tsx +++ b/src/pages/OnboardingWorkspaces/BaseOnboardingWorkspaces.tsx @@ -104,7 +104,11 @@ function BaseOnboardingWorkspaces({route, shouldUseNativeStyles}: BaseOnboarding setOnboardingPolicyID(policy.policyID); if (shouldUseSubmitFlow) { - navigateToSubmitWorkspaceAfterOnboardingWithMicrotaskQueue(policy.policyID, shouldUseNarrowLayout); + if (conciergeReportID) { + navigateToSubmitWorkspaceAfterOnboardingWithMicrotaskQueue(policy.policyID, shouldUseNarrowLayout, conciergeReportID); + } else { + navigateToSubmitWorkspaceAfterOnboardingWithMicrotaskQueue(policy.policyID, shouldUseNarrowLayout); + } return; } diff --git a/tests/unit/libs/navigateAfterOnboarding.test.ts b/tests/unit/libs/navigateAfterOnboarding.test.ts index 864b93742c1c..5c055200c206 100644 --- a/tests/unit/libs/navigateAfterOnboarding.test.ts +++ b/tests/unit/libs/navigateAfterOnboarding.test.ts @@ -2,6 +2,7 @@ import SidePanelActions from '@libs/actions/SidePanel'; import {setOnboardingRHPVariant} from '@libs/actions/Welcome'; import {navigateToSubmitWorkspaceAfterOnboardingWithMicrotaskQueue} from '@libs/navigateAfterOnboarding'; import Navigation from '@libs/Navigation/Navigation'; +import {clearPendingConciergeDeepLink, setPendingConciergeDeepLink} from '@libs/PendingConciergeDeepLink'; import {buildCannedSearchQuery} from '@libs/SearchQueryUtils'; import CONST from '@src/CONST'; @@ -34,6 +35,7 @@ const navigationMock = jest.mocked(Navigation); describe('navigateToSubmitWorkspaceAfterOnboardingWithMicrotaskQueue', () => { beforeEach(() => { jest.clearAllMocks(); + clearPendingConciergeDeepLink(); }); it('navigates to HOME without opening the side panel when policyID is missing', () => { @@ -67,4 +69,16 @@ describe('navigateToSubmitWorkspaceAfterOnboardingWithMicrotaskQueue', () => { expect(setOnboardingRHPVariant).toHaveBeenCalledWith(CONST.ONBOARDING_RHP_VARIANT.RHP_ADMINS_ROOM); expect(SidePanelActions.openSidePanel).toHaveBeenCalledWith(false); }); + + it('navigates to Concierge when Submit onboarding started from a pending Concierge deep link', () => { + setPendingConciergeDeepLink(); + + navigateToSubmitWorkspaceAfterOnboardingWithMicrotaskQueue('test-policy-id', false, 'concierge-report-id'); + + expect(navigationMock.dismissModal).toHaveBeenCalledTimes(1); + expect(navigationMock.navigate).toHaveBeenCalledTimes(1); + expect(navigationMock.navigate).toHaveBeenCalledWith(ROUTES.REPORT_WITH_ID.getRoute('concierge-report-id')); + expect(setOnboardingRHPVariant).not.toHaveBeenCalled(); + expect(SidePanelActions.openSidePanel).not.toHaveBeenCalled(); + }); }); diff --git a/tests/unit/navigateAfterOnboardingTest.ts b/tests/unit/navigateAfterOnboardingTest.ts index fa80e72e0767..71ee330d14e2 100644 --- a/tests/unit/navigateAfterOnboardingTest.ts +++ b/tests/unit/navigateAfterOnboardingTest.ts @@ -1,5 +1,6 @@ import {navigateAfterOnboarding} from '@libs/navigateAfterOnboarding'; import Navigation from '@libs/Navigation/Navigation'; +import {clearPendingConciergeDeepLink, setPendingConciergeDeepLink} from '@libs/PendingConciergeDeepLink'; import type * as ReportUtils from '@libs/ReportUtils'; import initOnyxDerivedValues from '@userActions/OnyxDerived'; @@ -89,6 +90,7 @@ describe('navigateAfterOnboarding', () => { beforeEach(async () => { jest.clearAllMocks(); mockIsReportTopmostSplitNavigator.mockReturnValue(false); + clearPendingConciergeDeepLink(); return Onyx.clear(); }); @@ -191,4 +193,35 @@ describe('navigateAfterOnboarding', () => { navigateAfterOnboarding(false, true, '', {}, undefined, ONBOARDING_ADMINS_CHAT_REPORT_ID, false, {variantOverride: CONST.ONBOARDING_RHP_VARIANT.INBOX_ADMINS_BESPOKE}); expect(navigate).toHaveBeenCalledWith(ROUTES.REPORT_WITH_ID.getRoute(ONBOARDING_ADMINS_CHAT_REPORT_ID), undefined); }); + + it('should navigate to Concierge instead of Home when a pending Concierge deep link is available', () => { + const navigate = jest.spyOn(Navigation, 'navigate'); + setPendingConciergeDeepLink(); + + navigateAfterOnboarding(false, true, REPORT_ID, {}, undefined, undefined); + + expect(navigate).toHaveBeenCalledWith(ROUTES.REPORT_WITH_ID.getRoute(REPORT_ID), undefined); + expect(navigate).not.toHaveBeenCalledWith(ROUTES.HOME); + }); + + it('should navigate to Concierge route when pending deep link is set but conciergeReportID is empty', () => { + const navigate = jest.spyOn(Navigation, 'navigate'); + setPendingConciergeDeepLink(); + + navigateAfterOnboarding(false, true, '', {}, undefined, undefined); + + expect(navigate).toHaveBeenCalledWith(ROUTES.CONCIERGE, undefined); + expect(navigate).not.toHaveBeenCalledWith(ROUTES.HOME); + }); + + it('should consume the pending Concierge deep link after onboarding navigation', () => { + const navigate = jest.spyOn(Navigation, 'navigate'); + setPendingConciergeDeepLink(); + + navigateAfterOnboarding(false, true, REPORT_ID, {}, undefined, undefined); + navigateAfterOnboarding(false, true, REPORT_ID, {}, undefined, undefined); + + expect(navigate).toHaveBeenNthCalledWith(1, ROUTES.REPORT_WITH_ID.getRoute(REPORT_ID), undefined); + expect(navigate).toHaveBeenNthCalledWith(2, ROUTES.HOME, undefined); + }); });