Skip to content
126 changes: 88 additions & 38 deletions src/libs/actions/Link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import buildOldDotURL from '@libs/buildOldDotURL';
import getIsNarrowLayout from '@libs/getIsNarrowLayout';
import isPublicScreenRoute from '@libs/isPublicScreenRoute';
import Log from '@libs/Log';
import {setDeepLinkToOpenAfterOnboarding} from '@libs/navigateAfterOnboarding';
import getStateFromPath from '@libs/Navigation/helpers/getStateFromPath';
import {hasJoinWorkspaceTaskParam} from '@libs/Navigation/helpers/isJoinWorkspaceTaskPath';
import {isOnboardingFlowName} from '@libs/Navigation/helpers/isNavigatorName';
Expand Down Expand Up @@ -568,35 +569,10 @@ function openReportFromDeepLink(
const state = navigationRef.getRootState();
const currentFocusedRoute = findFocusedRoute(state);

if (isOnboardingFlowName(currentFocusedRoute?.name)) {
if (hasCompletedGuidedSetupFlowSelector(val) === false) {
setOnboardingErrorMessage('onboarding.purpose.errorBackButton');
}
return;
}

if (shouldSkipDeepLinkNavigation(route)) {
return;
}

if (currentFocusedRoute?.name !== SCREENS.HOME && route === ROUTES.HOME) {
return;
}

// Drop a deep link captured before onboarding finished: navigateAfterOnboarding owns the
// post-onboarding destination and overrides it anyway, so honoring it only risks the flash (#91437).
if (initialHasCompletedGuidedSetupFlow === false) {
return;
}

// Navigation for signed users is handled by react-navigation.
if (isAuthenticated) {
return;
}
const deeplinkRoute = route as Route;

const navigateHandler = (reportParam?: OnyxEntry<Report>) => {
// Skip if the user already is in the deeplinked route.
const deeplinkRoute = route as Route;
if (deeplinkRoute && Navigation.isActiveRoute(deeplinkRoute)) {
return;
}
Expand Down Expand Up @@ -628,22 +604,96 @@ function openReportFromDeepLink(
const forceReplace = navigationRef.getRootState().routes.at(-1)?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR;
Navigation.navigate(deeplinkRoute, {forceReplace, waitForTransition: true});
};
// If we log with deeplink with reportID and data for this report is not available yet,
// then we will wait for Onyx to completely merge data from OpenReport API with OpenApp API in AuthScreens
if (reportID && !isAuthenticated && !reports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]?.reportID) {
const reportConnection = Onyx.connectWithoutView({
key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
// eslint-disable-next-line rulesdir/prefer-early-return
callback: (report) => {
if (report?.errorFields?.notFound || report?.reportID || (report === undefined && CONST.REGEX.NON_NUMERIC.test(reportID))) {

const openDeepLink = () => {
// OpenApp and OpenReport may not have written this report yet, and navigateHandler would read the missing
// report as "does not exist" and send the user to the last accessed report or Concierge instead.
if (reportID && !isAuthenticated && !reports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`]?.reportID) {
const reportConnection = Onyx.connectWithoutView({
key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
callback: (report) => {
if (!report?.errorFields?.notFound && !report?.reportID && !(report === undefined && CONST.REGEX.NON_NUMERIC.test(reportID))) {
return;
}
Onyx.disconnect(reportConnection);
navigateHandler(report);
}
},
});
} else {
},
});
return;
}

navigateHandler();
};

// Shared with the onboarding branch below, which sits above this check and would otherwise let a parked link
// replay past it.
const shouldDropDeepLink = () => shouldSkipDeepLinkNavigation(route) || (currentFocusedRoute?.name !== SCREENS.HOME && route === ROUTES.HOME);

// Opening the link while onboarding still owns the screen flashes the "Not here" page, so hand it to
// navigateAfterOnboarding, which runs after the modal is gone and would otherwise lose it.
const deferUntilAfterOnboarding = () => {
if (initialHasCompletedGuidedSetupFlow !== false || shouldDropDeepLink()) {
return false;
}

setDeepLinkToOpenAfterOnboarding(() => {
// A new account can finish onboarding before OpenApp does. Opening the link then shows its loading
// skeleton, so wait until report data has loaded. This runs in navigation code, outside React, so
// useOnyx is not available.
const loadingConnection = Onyx.connectWithoutView({
key: ONYXKEYS.IS_LOADING_REPORT_DATA,
callback: (isLoadingReportData) => {
if (isLoadingReportData) {
return;
}
Onyx.disconnect(loadingConnection);

// The concierge route is a URL entry page that redirects itself, and on wide layouts it slides in as a
Comment thread
mukhrr marked this conversation as resolved.
// panel first, so open the chat the way the rest of the app does instead.
if (findFocusedRoute(getStateFromPath(deeplinkRoute))?.name === SCREENS.CONCIERGE) {
navigateToConciergeChat({
conciergeReportID,
introSelected,
currentUserAccountID,
isSelfTourViewed,
hasCompletedGuidedSetupFlow: hasCompletedGuidedSetupFlowSelector(val),
conciergePersonalDetails,
});
return;
}
openDeepLink();
},
});
return true;
});
return true;
};

if (isOnboardingFlowName(currentFocusedRoute?.name)) {
if (deferUntilAfterOnboarding()) {
return;
}

if (hasCompletedGuidedSetupFlowSelector(val) === false) {
setOnboardingErrorMessage('onboarding.purpose.errorBackButton');
}
return;
}

if (shouldDropDeepLink()) {
return;
}

// Navigation for signed users is handled by react-navigation.
if (isAuthenticated) {
return;
}

if (deferUntilAfterOnboarding()) {
return;
}

openDeepLink();
};

if (hasCompletedGuidedSetupFlowSelector(val) || isAnonymousUser()) {
Expand Down
41 changes: 40 additions & 1 deletion src/libs/navigateAfterOnboarding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ import Onyx from 'react-native-onyx';
import {setDisableDismissOnEscape} from './actions/Modal';
import SidePanelActions from './actions/SidePanel';
import {setOnboardingRHPVariant} from './actions/Welcome';
import Log from './Log';
import isReportTopmostSplitNavigator from './Navigation/helpers/isReportTopmostSplitNavigator';
import dismissOnboardingModalBeforeExit from './Navigation/helpers/OnboardingNavigationUtils';
import shouldOpenOnAdminRoom from './Navigation/helpers/shouldOpenOnAdminRoom';
import Navigation from './Navigation/Navigation';
import {findLastAccessedReport} from './ReportUtils';
import {buildCannedSearchQuery} from './SearchQueryUtils';
import {registerSessionCleanupCallback} from './SessionCleanup';

let onboardingRHPVariant: OnyxEntry<OnboardingRHPVariant>;
Onyx.connectWithoutView({
Expand All @@ -27,6 +29,34 @@ Onyx.connectWithoutView({
},
});

/** Parked by openReportFromDeepLink() while onboarding owns the screen. In memory rather than Onyx so a stale destination cannot replay after a reload or for the next account on the device. */
let openDeepLinkAfterOnboarding: (() => boolean) | undefined;

/** The callback returns whether it opened the route. Returning false lets the caller pick the default destination. */
function setDeepLinkToOpenAfterOnboarding(callback: () => boolean) {
openDeepLinkAfterOnboarding = callback;
}

function consumeDeepLinkToOpenAfterOnboarding(): boolean {
if (!openDeepLinkAfterOnboarding) {
return false;
}

const openDeepLink = openDeepLinkAfterOnboarding;
openDeepLinkAfterOnboarding = undefined;
try {
return openDeepLink();
} catch (error) {
// The onboarding modal is already dismissed, so a failing replay must not take the default destination with it.
Log.alert('[navigateAfterOnboarding] Parked deep link failed to open', {error});
return false;
}
}

registerSessionCleanupCallback(() => {
openDeepLinkAfterOnboarding = undefined;
});

type NavigateAfterOnboardingOptions = {
afterTransition?: () => void;
variantOverride?: OnboardingRHPVariant | null;
Expand Down Expand Up @@ -86,6 +116,11 @@ function navigateAfterOnboarding(
) {
setDisableDismissOnEscape(false);

// A route the user asked for before signing up outranks every default below, including the Side Panel variant.
if (consumeDeepLinkToOpenAfterOnboarding()) {
return;
}

// 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
Expand Down Expand Up @@ -163,6 +198,10 @@ function navigateAfterOnboardingWithMicrotaskQueue(
function navigateToSubmitWorkspaceAfterOnboarding(policyID?: string, shouldUseNarrowLayout = false) {
setDisableDismissOnEscape(false);

if (consumeDeepLinkToOpenAfterOnboarding()) {
return;
}

if (!policyID) {
Navigation.navigate(ROUTES.HOME);
return;
Expand All @@ -181,4 +220,4 @@ function navigateToSubmitWorkspaceAfterOnboardingWithMicrotaskQueue(policyID?: s
});
}

export {navigateAfterOnboarding, navigateAfterOnboardingWithMicrotaskQueue, navigateToSubmitWorkspaceAfterOnboardingWithMicrotaskQueue};
export {navigateAfterOnboarding, navigateAfterOnboardingWithMicrotaskQueue, setDeepLinkToOpenAfterOnboarding, navigateToSubmitWorkspaceAfterOnboardingWithMicrotaskQueue};
50 changes: 48 additions & 2 deletions src/pages/ConciergePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,30 @@ import useThemeStyles from '@hooks/useThemeStyles';

import {navigateToConciergeChat} from '@libs/actions/Report';
import Navigation from '@libs/Navigation/Navigation';
import navigationRef from '@libs/Navigation/navigationRef';

import CONFIG from '@src/CONFIG';
import CONST from '@src/CONST';
import NAVIGATORS from '@src/NAVIGATORS';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';

import type {OnyxValue} from 'react-native-onyx';

import {useFocusEffect} from '@react-navigation/native';
import {guidedSetupAndTourStatusSelector} from '@selectors/Onboarding';
import {isActingAsDelegateSelector} from '@selectors/Account';
import {isSingleNewDotEntrySelector} from '@selectors/HybridApp';
import {guidedSetupAndTourStatusSelector, tryNewDotOnyxSelector, wasInvitedToNewDotSelector} from '@selectors/Onboarding';
import {isSupportalSessionSelector} from '@selectors/Session';
import React, {useCallback, useEffect, useRef} from 'react';
import {View} from 'react-native';

const isMigratedUserSelector = (tryNewDot: OnyxValue<typeof ONYXKEYS.NVP_TRY_NEW_DOT>) => tryNewDotOnyxSelector(tryNewDot).hasBeenAddedToNudgeMigration;

function isOnboardingOpen() {
return navigationRef.isReady() && !!navigationRef.getRootState()?.routes.some((route) => route.name === NAVIGATORS.ONBOARDING_MODAL_NAVIGATOR);
}

/*
* This is a "utility page", that does this:
* - If the user is authenticated, find their concierge chat and re-route to it
Expand All @@ -36,6 +50,23 @@ function ConciergePage() {
const [guidedSetupAndTourStatus] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: guidedSetupAndTourStatusSelector});
const isSelfTourViewed = guidedSetupAndTourStatus?.isSelfTourViewed;
const hasCompletedGuidedSetupFlow = guidedSetupAndTourStatus?.hasCompletedGuidedSetupFlow;
const [hasNonPersonalPolicy] = useOnyx(ONYXKEYS.HAS_NON_PERSONAL_POLICY);
const [isMigratedUser] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT, {selector: isMigratedUserSelector});
const [isActingAsDelegate] = useOnyx(ONYXKEYS.ACCOUNT, {selector: isActingAsDelegateSelector});
const [isSingleNewDotEntry] = useOnyx(ONYXKEYS.HYBRID_APP, {selector: isSingleNewDotEntrySelector});

// The accounts useOnboardingFlow never sends to onboarding, so their flag can stay false for good.
Comment thread
mukhrr marked this conversation as resolved.
const isOnboardingSkipped =
!!hasNonPersonalPolicy ||
!!wasInvitedToNewDotSelector(introSelected) ||
!!isMigratedUser ||
isSupportalSessionSelector(session) ||
!!isActingAsDelegate ||
(CONFIG.IS_HYBRID_APP && !!isSingleNewDotEntry);

// Onboarding opens on top of this page for a new account, and the redirect below dismisses the top modal, so it
Comment thread
mukhrr marked this conversation as resolved.
// would close onboarding before it is finished. The link is opened once onboarding completes instead.
const isWaitingForOnboarding = !CONFIG.SKIP_ONBOARDING && hasCompletedGuidedSetupFlow === false;
Comment thread
mukhrr marked this conversation as resolved.

useFocusEffect(
useCallback(() => {
Expand All @@ -45,6 +76,11 @@ function ConciergePage() {
return;
}

// Onboarding can join or create a workspace before it finishes, so an open onboarding still wins.
if (isWaitingForOnboarding && (!isOnboardingSkipped || isOnboardingOpen())) {
return;
}

navigateToConciergeChat({
conciergeReportID,
introSelected,
Expand All @@ -59,7 +95,17 @@ function ConciergePage() {
} else {
Navigation.navigate(ROUTES.INBOX);
}
}, [session, isLoadingReportData, conciergeReportID, introSelected, isSelfTourViewed, hasCompletedGuidedSetupFlow, conciergePersonalDetails]),
}, [
session,
isLoadingReportData,
conciergeReportID,
introSelected,
isSelfTourViewed,
hasCompletedGuidedSetupFlow,
conciergePersonalDetails,
isWaitingForOnboarding,
isOnboardingSkipped,
]),
);

useEffect(() => {
Expand Down
Loading
Loading