Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
c307c84
fix: navigate to Concierge chat after login from deep link on web
x-dev90 Jul 17, 2026
f870138
Fix missing pending Concierge deep link helper
x-dev90 Jul 17, 2026
9f44d64
Merge branch 'main' into fix/web-concierge-deep-link-login-navigation
x-dev90 Jul 17, 2026
b972082
Add comment for pending Concierge intent helper
x-dev90 Jul 17, 2026
0ceaa6f
Merge branch 'main' into fix/web-concierge-deep-link-login-navigation
x-dev90 Jul 18, 2026
e6df449
fix: persist pending Concierge deep link across reloads
x-dev90 Jul 18, 2026
91e65ec
fix: use type import for pending Concierge reload test
x-dev90 Jul 18, 2026
537a2e3
Merge branch 'main' into fix/web-concierge-deep-link-login-navigation
x-dev90 Jul 21, 2026
eea9b8d
Merge branch 'main' into fix/web-concierge-deep-link-login-navigation
x-dev90 Jul 21, 2026
3f4a0eb
Fix Concierge onboarding deep link persistence and cancellation
x-dev90 Jul 21, 2026
74735c9
Fix lint-safe Concierge deep link reload detection
x-dev90 Jul 21, 2026
c2cf6b2
Merge branch 'main' into fix/web-concierge-deep-link-login-navigation
x-dev90 Jul 22, 2026
71c10c7
Fix Concierge deep-link intent through onboarding edge cases
x-dev90 Jul 22, 2026
e181a8c
Fix Concierge deep link after onboarding refresh flows
x-dev90 Jul 25, 2026
b66c1b6
Merge branch 'main' into fix/web-concierge-deep-link-login-navigation
x-dev90 Jul 25, 2026
a5eb8b0
Merge branch 'main' into fix/web-concierge-deep-link-login-navigation
x-dev90 Jul 25, 2026
60d03b2
Merge branch 'main' into fix/web-concierge-deep-link-login-navigation
x-dev90 Jul 27, 2026
ee4b83b
Fix onboarding navigation tests after merge updates
x-dev90 Jul 27, 2026
9dfe564
Fix onboarding navigation test after merge updates
x-dev90 Jul 27, 2026
5198605
Merge branch 'main' into fix/web-concierge-deep-link-login-navigation
x-dev90 Jul 28, 2026
09f0907
Merge branch 'main' into fix/web-concierge-deep-link-login-navigation
x-dev90 Jul 29, 2026
10a4afe
Merge branch 'main' into fix/web-concierge-deep-link-login-navigation
x-dev90 Jul 30, 2026
39af2f4
Merge branch 'main' into fix/web-concierge-deep-link-login-navigation
x-dev90 Jul 30, 2026
c1d2933
Fix Concierge deep link before onboarding modal unmounts
x-dev90 Jul 30, 2026
2e8af74
Fix two-tab Concierge onboarding intent cancellation
x-dev90 Jul 30, 2026
e2adfa8
Fix unsafe finally return in Track onboarding navigation
x-dev90 Jul 30, 2026
033c831
fix: prevent Concierge flash after root signup onboarding
x-dev90 Jul 30, 2026
6f7abdb
fix: reset Escape dismiss state before onboarding deep-link navigation
x-dev90 Jul 30, 2026
ba32e0f
Merge branch 'main' into fix/web-concierge-deep-link-login-navigation
x-dev90 Aug 1, 2026
4701899
fix: keep Concierge deep link focused through onboarding
x-dev90 Aug 1, 2026
2302dca
fix: make Track onboarding cleanup lint-safe
x-dev90 Aug 1, 2026
7b7175b
Merge branch 'main' into fix/web-concierge-deep-link-login-navigation
x-dev90 Aug 1, 2026
0c58c36
fix: keep Concierge deep link through onboarding refresh
x-dev90 Aug 2, 2026
0a88195
fix: clear Concierge intent on explicit root signup flow
x-dev90 Aug 2, 2026
c52872a
fix: separate DeepLinkHandler route type import
x-dev90 Aug 2, 2026
54345c4
Merge branch 'main' into fix/web-concierge-deep-link-login-navigation
x-dev90 Aug 7, 2026
5d4c0d8
fix: navigate to Concierge chat after login from deep link on web
x-dev90 Aug 7, 2026
18c6687
fix: honor /concierge deep link through all onboarding exit paths
x-dev90 Aug 7, 2026
375995f
Fix KNIP regression for onboarding navigation helpers
x-dev90 Aug 7, 2026
985c346
Fix onboarding variant typing
x-dev90 Aug 7, 2026
dbbe184
Merge branch 'main' into fix/web-concierge-deep-link-login-navigation
x-dev90 Aug 7, 2026
fe46d80
Revert later PR changes and preserve only PR changes through f870138
x-dev90 Aug 8, 2026
ff8aa00
fix: resolve Knip unused export in onboarding navigation
x-dev90 Aug 8, 2026
5fc74dc
fix: prioritize pending Concierge deep link after onboarding
x-dev90 Aug 8, 2026
dbd6329
Merge branch 'main' into fix/web-concierge-deep-link-login-navigation
x-dev90 Aug 10, 2026
501bac9
fix: restore secure link handling in deep links
x-dev90 Aug 10, 2026
824126c
fix: preserve Concierge deep link through onboarding
x-dev90 Aug 10, 2026
737f793
Merge branch 'main' into fix/web-concierge-deep-link-login-navigation
x-dev90 Aug 17, 2026
75ddd25
Fix Concierge onboarding route type safety
x-dev90 Aug 17, 2026
8c7823f
Preserve Concierge deep link after Submit onboarding
x-dev90 Aug 17, 2026
c741163
Avoid undefined Concierge argument in Submit onboarding navigation
x-dev90 Aug 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/hooks/useAutoCreateSubmitWorkspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,11 @@ function useAutoCreateSubmitWorkspace() {
policyIDForNavigation = existingSubmitPolicyID;
}

navigateToSubmitWorkspaceAfterOnboardingWithMicrotaskQueue(policyIDForNavigation, shouldUseNarrowLayout);
if (conciergeReportID) {
navigateToSubmitWorkspaceAfterOnboardingWithMicrotaskQueue(policyIDForNavigation, shouldUseNarrowLayout, conciergeReportID);
} else {
navigateToSubmitWorkspaceAfterOnboardingWithMicrotaskQueue(policyIDForNavigation, shouldUseNarrowLayout);
}
},
[
currentUserEmail,
Expand All @@ -148,6 +152,7 @@ function useAutoCreateSubmitWorkspace() {
hasActiveAdminPolicies,
shouldUseNarrowLayout,
conciergeChat,
conciergeReportID,
],
);

Expand Down
17 changes: 17 additions & 0 deletions src/libs/PendingConciergeDeepLink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
let hasPendingConciergeDeepLink = false;
Comment thread
x-dev90 marked this conversation as resolved.
Comment thread
x-dev90 marked this conversation as resolved.

function setPendingConciergeDeepLink() {
hasPendingConciergeDeepLink = true;
}

function consumePendingConciergeDeepLink() {
const shouldNavigateToConcierge = hasPendingConciergeDeepLink;
hasPendingConciergeDeepLink = false;
return shouldNavigateToConcierge;
}

function clearPendingConciergeDeepLink() {
hasPendingConciergeDeepLink = false;
}

export {setPendingConciergeDeepLink, consumePendingConciergeDeepLink, clearPendingConciergeDeepLink};
15 changes: 15 additions & 0 deletions src/libs/actions/Link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<Report>,
Expand Down Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions src/libs/actions/SignInRedirect.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -48,6 +49,8 @@ Onyx.connectWithoutView({
});

function clearStorageAndRedirect(errorMessage?: string, isSAMLReauthentication?: boolean): Promise<void> {
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.
Expand Down
24 changes: 20 additions & 4 deletions src/libs/navigateAfterOnboarding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand All @@ -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);
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
14 changes: 14 additions & 0 deletions tests/unit/libs/navigateAfterOnboarding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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();
});
});
33 changes: 33 additions & 0 deletions tests/unit/navigateAfterOnboardingTest.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -89,6 +90,7 @@ describe('navigateAfterOnboarding', () => {
beforeEach(async () => {
jest.clearAllMocks();
mockIsReportTopmostSplitNavigator.mockReturnValue(false);
clearPendingConciergeDeepLink();
return Onyx.clear();
});

Expand Down Expand Up @@ -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);
});
});
Loading