diff --git a/src/components/TestDrive/TestDriveDemo.tsx b/src/components/TestDrive/TestDriveDemo.tsx
index 41bf6c0ce630a..a02296815ff60 100644
--- a/src/components/TestDrive/TestDriveDemo.tsx
+++ b/src/components/TestDrive/TestDriveDemo.tsx
@@ -4,12 +4,14 @@ import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOffli
import EmbeddedDemo from '@components/EmbeddedDemo';
import Modal from '@components/Modal';
import SafeAreaConsumer from '@components/SafeAreaConsumer';
+import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useOnboardingMessages from '@hooks/useOnboardingMessages';
import useOnyx from '@hooks/useOnyx';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import {completeTestDriveTask} from '@libs/actions/Task';
import Navigation from '@libs/Navigation/Navigation';
+import {isPaidGroupPolicy, isUserPolicyAdmin} from '@libs/PolicyUtils';
import {isAdminRoom} from '@libs/ReportUtils';
import {getTestDriveURL} from '@libs/TourUtils';
import CONST from '@src/CONST';
@@ -25,6 +27,11 @@ function TestDriveDemo() {
const [onboardingReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${onboarding?.chatReportID}`, {canBeMissing: true});
const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true});
const {testDrive} = useOnboardingMessages();
+ const currentUserPersonalDetails = useCurrentUserPersonalDetails();
+ const [isPolicyAdmin = false] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {
+ canBeMissing: true,
+ selector: (policies) => Object.values(policies ?? {}).some((policy) => isPaidGroupPolicy(policy) && isUserPolicyAdmin(policy, currentUserPersonalDetails.login)),
+ });
useEffect(() => {
InteractionManager.runAfterInteractions(() => {
@@ -62,7 +69,7 @@ function TestDriveDemo() {
diff --git a/src/hooks/useOnboardingFlow.ts b/src/hooks/useOnboardingFlow.ts
index 9d998965191d8..dd5bea40b29dc 100644
--- a/src/hooks/useOnboardingFlow.ts
+++ b/src/hooks/useOnboardingFlow.ts
@@ -2,12 +2,13 @@ import {useEffect, useRef} from 'react';
import {InteractionManager} from 'react-native';
import {startOnboardingFlow} from '@libs/actions/Welcome/OnboardingFlow';
import getCurrentUrl from '@libs/Navigation/currentUrl';
-import Navigation from '@libs/Navigation/Navigation';
+import Navigation, {navigationRef} from '@libs/Navigation/Navigation';
import {hasCompletedGuidedSetupFlowSelector, tryNewDotOnyxSelector} from '@libs/onboardingSelectors';
import {buildCannedSearchQuery} from '@libs/SearchQueryUtils';
import {isLoggingInAsNewUser} from '@libs/SessionUtils';
import isProductTrainingElementDismissed from '@libs/TooltipUtils';
import CONFIG from '@src/CONFIG';
+import NAVIGATORS from '@src/NAVIGATORS';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
@@ -61,11 +62,17 @@ function useOnboardingFlowRouter() {
if (CONFIG.IS_HYBRID_APP && isLoadingOnyxValue(isSingleNewDotEntryMetadata)) {
return;
}
+
if (hasBeenAddedToNudgeMigration && !isProductTrainingElementDismissed('migratedUserWelcomeModal', dismissedProductTraining)) {
- const defaultCannedQuery = buildCannedSearchQuery();
- const query = defaultCannedQuery;
- Navigation.navigate(ROUTES.SEARCH_ROOT.getRoute({query}));
- Navigation.navigate(ROUTES.MIGRATED_USER_WELCOME_MODAL.getRoute(true));
+ const navigationState = navigationRef.getRootState();
+ const lastRoute = navigationState.routes.at(-1);
+ // Prevent duplicate navigation if the migrated user modal is already shown.
+ if (lastRoute?.name !== NAVIGATORS.MIGRATED_USER_MODAL_NAVIGATOR) {
+ const defaultCannedQuery = buildCannedSearchQuery();
+ const query = defaultCannedQuery;
+ Navigation.navigate(ROUTES.SEARCH_ROOT.getRoute({query}));
+ Navigation.navigate(ROUTES.MIGRATED_USER_WELCOME_MODAL.getRoute(true));
+ }
return;
}
diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts
index f2175e789f58e..2e1edba6dabbb 100644
--- a/src/libs/PolicyUtils.ts
+++ b/src/libs/PolicyUtils.ts
@@ -311,7 +311,7 @@ function isExpensifyTeam(email: string | undefined): boolean {
/**
* Checks if the user with login is an admin of the policy.
*/
-const isUserPolicyAdmin = (policy: OnyxInputOrEntry, login?: string) => !!(policy && policy.employeeList && login && policy.employeeList[login]?.role === CONST.POLICY.ROLE.ADMIN);
+const isUserPolicyAdmin = (policy: OnyxInputOrEntry, login?: string) => getPolicyRole(policy, login) === CONST.POLICY.ROLE.ADMIN;
/**
* Checks if the current user is of the role "user" on the policy.
diff --git a/src/libs/TourUtils.ts b/src/libs/TourUtils.ts
index 1fcf3a6683ee3..882845510c046 100644
--- a/src/libs/TourUtils.ts
+++ b/src/libs/TourUtils.ts
@@ -1,16 +1,27 @@
+import type {OnyxEntry} from 'react-native-onyx';
import CONST from '@src/CONST';
import type {IntroSelected} from './actions/Report';
-function getTestDriveURL(shouldUseNarrowLayout: boolean, introSelected?: IntroSelected) {
- if (introSelected?.choice === CONST.ONBOARDING_CHOICES.SUBMIT && introSelected.inviteType === CONST.ONBOARDING_INVITE_TYPES.WORKSPACE) {
- return shouldUseNarrowLayout ? CONST.STORYLANE.EMPLOYEE_TOUR_MOBILE : CONST.STORYLANE.EMPLOYEE_TOUR;
+function getTestDriveURL(shouldUseNarrowLayout: boolean, introSelected: OnyxEntry, isUserPolicyAdmin: boolean): string {
+ if (introSelected) {
+ if (introSelected?.choice === CONST.ONBOARDING_CHOICES.SUBMIT && introSelected.inviteType === CONST.ONBOARDING_INVITE_TYPES.WORKSPACE) {
+ return shouldUseNarrowLayout ? CONST.STORYLANE.EMPLOYEE_TOUR_MOBILE : CONST.STORYLANE.EMPLOYEE_TOUR;
+ }
+
+ if (introSelected?.choice === CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE) {
+ return shouldUseNarrowLayout ? CONST.STORYLANE.TRACK_WORKSPACE_TOUR_MOBILE : CONST.STORYLANE.TRACK_WORKSPACE_TOUR;
+ }
+
+ return shouldUseNarrowLayout ? CONST.STORYLANE.ADMIN_TOUR_MOBILE : CONST.STORYLANE.ADMIN_TOUR;
}
- if (introSelected?.choice === CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE) {
- return shouldUseNarrowLayout ? CONST.STORYLANE.TRACK_WORKSPACE_TOUR_MOBILE : CONST.STORYLANE.TRACK_WORKSPACE_TOUR;
+ // Migrated users don't have the introSelected NVP, so we must check if they are an Admin of any Workspace in order
+ // to show the Admin demo.
+ if (isUserPolicyAdmin) {
+ return shouldUseNarrowLayout ? CONST.STORYLANE.ADMIN_TOUR_MOBILE : CONST.STORYLANE.ADMIN_TOUR;
}
- return shouldUseNarrowLayout ? CONST.STORYLANE.ADMIN_TOUR_MOBILE : CONST.STORYLANE.ADMIN_TOUR;
+ return shouldUseNarrowLayout ? CONST.STORYLANE.EMPLOYEE_TOUR_MOBILE : CONST.STORYLANE.EMPLOYEE_TOUR;
}
// eslint-disable-next-line import/prefer-default-export
diff --git a/src/libs/actions/Tour.ts b/src/libs/actions/Tour.ts
new file mode 100644
index 0000000000000..895b35013cef3
--- /dev/null
+++ b/src/libs/actions/Tour.ts
@@ -0,0 +1,26 @@
+import {InteractionManager} from 'react-native';
+import Navigation from '@libs/Navigation/Navigation';
+import CONST from '@src/CONST';
+import ROUTES from '@src/ROUTES';
+import type {IntroSelected} from './Report';
+import {completeTestDriveTask} from './Task';
+
+function startTestDrive(introSelected: IntroSelected | undefined, shouldUpdateSelfTourViewedOnlyLocally = false, hasUserBeenAddedToNudgeMigration = false) {
+ InteractionManager.runAfterInteractions(() => {
+ if (
+ hasUserBeenAddedToNudgeMigration ||
+ introSelected?.choice === CONST.ONBOARDING_CHOICES.MANAGE_TEAM ||
+ introSelected?.choice === CONST.ONBOARDING_CHOICES.TEST_DRIVE_RECEIVER ||
+ introSelected?.choice === CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE ||
+ (introSelected?.choice === CONST.ONBOARDING_CHOICES.SUBMIT && introSelected.inviteType === CONST.ONBOARDING_INVITE_TYPES.WORKSPACE)
+ ) {
+ completeTestDriveTask(shouldUpdateSelfTourViewedOnlyLocally);
+ Navigation.navigate(ROUTES.TEST_DRIVE_DEMO_ROOT);
+ } else {
+ Navigation.navigate(ROUTES.TEST_DRIVE_MODAL_ROOT.route);
+ }
+ });
+}
+
+// eslint-disable-next-line import/prefer-default-export
+export {startTestDrive};
diff --git a/src/pages/Search/EmptySearchView.tsx b/src/pages/Search/EmptySearchView.tsx
index 8a11eb666f6c7..1955602e6e911 100644
--- a/src/pages/Search/EmptySearchView.tsx
+++ b/src/pages/Search/EmptySearchView.tsx
@@ -1,7 +1,7 @@
import React, {useMemo, useRef, useState} from 'react';
// eslint-disable-next-line no-restricted-imports
import type {GestureResponderEvent, ImageStyle, Text as RNText, TextStyle, ViewStyle} from 'react-native';
-import {InteractionManager, Linking, View} from 'react-native';
+import {Linking, View} from 'react-native';
import type {OnyxCollection} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import BookTravelButton from '@components/BookTravelButton';
@@ -28,10 +28,10 @@ import useThemeStyles from '@hooks/useThemeStyles';
import {startMoneyRequest} from '@libs/actions/IOU';
import {openOldDotLink} from '@libs/actions/Link';
import {createNewReport} from '@libs/actions/Report';
-import {completeTestDriveTask} from '@libs/actions/Task';
+import {startTestDrive} from '@libs/actions/Tour';
import interceptAnonymousUser from '@libs/interceptAnonymousUser';
import Navigation from '@libs/Navigation/Navigation';
-import {hasSeenTourSelector} from '@libs/onboardingSelectors';
+import {hasSeenTourSelector, tryNewDotOnyxSelector} from '@libs/onboardingSelectors';
import {areAllGroupPoliciesExpenseChatDisabled, getGroupPaidPoliciesWithExpenseChatEnabled, isPaidGroupPolicy} from '@libs/PolicyUtils';
import {generateReportID} from '@libs/ReportUtils';
import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils';
@@ -90,6 +90,7 @@ function EmptySearchView({hash, type, groupBy, hasResults}: EmptySearchViewProps
const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {
canBeMissing: true,
});
+ const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT, {selector: tryNewDotOnyxSelector, canBeMissing: true});
const groupPoliciesWithChatEnabled = getGroupPaidPoliciesWithExpenseChatEnabled();
@@ -194,20 +195,8 @@ function EmptySearchView({hash, type, groupBy, hasResults}: EmptySearchViewProps
}
}
- const startTestDrive = () => {
- InteractionManager.runAfterInteractions(() => {
- if (
- introSelected?.choice === CONST.ONBOARDING_CHOICES.MANAGE_TEAM ||
- introSelected?.choice === CONST.ONBOARDING_CHOICES.TEST_DRIVE_RECEIVER ||
- introSelected?.choice === CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE ||
- (introSelected?.choice === CONST.ONBOARDING_CHOICES.SUBMIT && introSelected.inviteType === CONST.ONBOARDING_INVITE_TYPES.WORKSPACE)
- ) {
- completeTestDriveTask();
- Navigation.navigate(ROUTES.TEST_DRIVE_DEMO_ROOT);
- } else {
- Navigation.navigate(ROUTES.TEST_DRIVE_MODAL_ROOT.route);
- }
- });
+ const startTestDriveAction = () => {
+ startTestDrive(introSelected, false, tryNewDot?.hasBeenAddedToNudgeMigration);
};
// If we are grouping by reports, show a custom message rather than a type-specific message
@@ -229,7 +218,7 @@ function EmptySearchView({hash, type, groupBy, hasResults}: EmptySearchViewProps
? [
{
buttonText: translate('emptySearchView.takeATestDrive'),
- buttonAction: startTestDrive,
+ buttonAction: startTestDriveAction,
},
]
: []),
@@ -296,7 +285,7 @@ function EmptySearchView({hash, type, groupBy, hasResults}: EmptySearchViewProps
? [
{
buttonText: translate('emptySearchView.takeATestDrive'),
- buttonAction: startTestDrive,
+ buttonAction: startTestDriveAction,
},
]
: []),
@@ -327,7 +316,7 @@ function EmptySearchView({hash, type, groupBy, hasResults}: EmptySearchViewProps
? [
{
buttonText: translate('emptySearchView.takeATestDrive'),
- buttonAction: startTestDrive,
+ buttonAction: startTestDriveAction,
},
]
: []),
@@ -369,8 +358,7 @@ function EmptySearchView({hash, type, groupBy, hasResults}: EmptySearchViewProps
styles.emptyStateFolderWebStyles,
styles.textAlignLeft,
styles.tripEmptyStateLottieWebView,
- introSelected?.choice,
- introSelected?.inviteType,
+ introSelected,
hasResults,
defaultViewItemHeader,
hasSeenTour,
@@ -381,6 +369,7 @@ function EmptySearchView({hash, type, groupBy, hasResults}: EmptySearchViewProps
tripViewChildren,
shouldRedirectToExpensifyClassic,
transactions,
+ tryNewDot?.hasBeenAddedToNudgeMigration,
]);
return (
diff --git a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx
index 0cf834958bfc4..c19152bbcd2bd 100644
--- a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx
+++ b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx
@@ -3,7 +3,7 @@ import {Str} from 'expensify-common';
import type {ImageContentFit} from 'expo-image';
import type {ForwardedRef} from 'react';
import React, {forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
-import {InteractionManager, View} from 'react-native';
+import {View} from 'react-native';
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import ConfirmModal from '@components/ConfirmModal';
import {DelegateNoAccessContext} from '@components/DelegateNoAccessModalProvider';
@@ -27,13 +27,13 @@ import {openOldDotLink} from '@libs/actions/Link';
import {navigateToQuickAction} from '@libs/actions/QuickActionNavigation';
import {createNewReport, startNewChat} from '@libs/actions/Report';
import {isAnonymousUser} from '@libs/actions/Session';
-import {completeTestDriveTask} from '@libs/actions/Task';
+import {startTestDrive} from '@libs/actions/Tour';
import getIconForAction from '@libs/getIconForAction';
import interceptAnonymousUser from '@libs/interceptAnonymousUser';
import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute';
import navigateAfterInteraction from '@libs/Navigation/navigateAfterInteraction';
import Navigation from '@libs/Navigation/Navigation';
-import {hasSeenTourSelector} from '@libs/onboardingSelectors';
+import {hasSeenTourSelector, tryNewDotOnyxSelector} from '@libs/onboardingSelectors';
import {openTravelDotLink, shouldOpenTravelDotLinkWeb} from '@libs/openTravelDotLink';
import {
areAllGroupPoliciesExpenseChatDisabled,
@@ -135,6 +135,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, isT
selector: hasSeenTourSelector,
canBeMissing: true,
});
+ const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT, {selector: tryNewDotOnyxSelector, canBeMissing: true});
const groupPoliciesWithChatEnabled = getGroupPaidPoliciesWithExpenseChatEnabled();
@@ -525,22 +526,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, isT
iconStyles: styles.popoverIconCircle,
iconFill: theme.icon,
text: translate('testDrive.quickAction.takeATwoMinuteTestDrive'),
- onSelected: () =>
- interceptAnonymousUser(() => {
- InteractionManager.runAfterInteractions(() => {
- if (
- introSelected?.choice === CONST.ONBOARDING_CHOICES.MANAGE_TEAM ||
- introSelected?.choice === CONST.ONBOARDING_CHOICES.TEST_DRIVE_RECEIVER ||
- introSelected?.choice === CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE ||
- (introSelected?.choice === CONST.ONBOARDING_CHOICES.SUBMIT && introSelected.inviteType === CONST.ONBOARDING_INVITE_TYPES.WORKSPACE)
- ) {
- completeTestDriveTask(isAnonymousUser());
- Navigation.navigate(ROUTES.TEST_DRIVE_DEMO_ROOT);
- } else {
- Navigation.navigate(ROUTES.TEST_DRIVE_MODAL_ROOT.route);
- }
- });
- }),
+ onSelected: () => interceptAnonymousUser(() => startTestDrive(introSelected, isAnonymousUser(), tryNewDot?.hasBeenAddedToNudgeMigration)),
},
]
: []),
diff --git a/tests/unit/TourUtilsTest.ts b/tests/unit/TourUtilsTest.ts
index 47739ecc41d7e..ccd15cca1667b 100644
--- a/tests/unit/TourUtilsTest.ts
+++ b/tests/unit/TourUtilsTest.ts
@@ -3,45 +3,77 @@ import CONST from '@src/CONST';
describe('TourUtils', () => {
describe('getTestDriveURL', () => {
- describe('Invited employee', () => {
- it('returns proper URL when screen is narrow', () => {
- const url = getTestDriveURL(true, {choice: CONST.ONBOARDING_CHOICES.SUBMIT, inviteType: CONST.ONBOARDING_INVITE_TYPES.WORKSPACE});
+ describe('NewDot users with introSelected NVP', () => {
+ describe('Invited employee', () => {
+ it('returns proper URL when screen is narrow', () => {
+ const url = getTestDriveURL(true, {choice: CONST.ONBOARDING_CHOICES.SUBMIT, inviteType: CONST.ONBOARDING_INVITE_TYPES.WORKSPACE}, false);
- expect(url).toBe(CONST.STORYLANE.EMPLOYEE_TOUR_MOBILE);
- });
+ expect(url).toBe(CONST.STORYLANE.EMPLOYEE_TOUR_MOBILE);
+ });
- it('returns proper URL when screen is not narrow', () => {
- const url = getTestDriveURL(false, {choice: CONST.ONBOARDING_CHOICES.SUBMIT, inviteType: CONST.ONBOARDING_INVITE_TYPES.WORKSPACE});
+ it('returns proper URL when screen is not narrow', () => {
+ const url = getTestDriveURL(false, {choice: CONST.ONBOARDING_CHOICES.SUBMIT, inviteType: CONST.ONBOARDING_INVITE_TYPES.WORKSPACE}, false);
- expect(url).toBe(CONST.STORYLANE.EMPLOYEE_TOUR);
+ expect(url).toBe(CONST.STORYLANE.EMPLOYEE_TOUR);
+ });
});
- });
- describe('Intro selected is Track Workspace', () => {
- it('returns proper URL when screen is narrow', () => {
- const url = getTestDriveURL(true, {choice: CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE});
+ describe('Intro selected is Track Workspace', () => {
+ it('returns proper URL when screen is narrow', () => {
+ const url = getTestDriveURL(true, {choice: CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE}, false);
+
+ expect(url).toBe(CONST.STORYLANE.TRACK_WORKSPACE_TOUR_MOBILE);
+ });
- expect(url).toBe(CONST.STORYLANE.TRACK_WORKSPACE_TOUR_MOBILE);
+ it('returns proper URL when screen is not narrow', () => {
+ const url = getTestDriveURL(false, {choice: CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE}, false);
+
+ expect(url).toBe(CONST.STORYLANE.TRACK_WORKSPACE_TOUR);
+ });
});
- it('returns proper URL when screen is not narrow', () => {
- const url = getTestDriveURL(false, {choice: CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE});
+ describe('Default case - Admin tour', () => {
+ it('returns proper URL when screen is narrow', () => {
+ const url = getTestDriveURL(true, {}, false);
+
+ expect(url).toBe(CONST.STORYLANE.ADMIN_TOUR_MOBILE);
+ });
- expect(url).toBe(CONST.STORYLANE.TRACK_WORKSPACE_TOUR);
+ it('returns proper URL when screen is not narrow', () => {
+ const url = getTestDriveURL(false, {}, false);
+
+ expect(url).toBe(CONST.STORYLANE.ADMIN_TOUR);
+ });
});
});
- describe('Default case - Admin tour', () => {
- it('returns proper URL when screen is narrow', () => {
- const url = getTestDriveURL(true);
+ describe('Migrated users from Classic - no introSelected NVP', () => {
+ describe('User is admin of a workspace', () => {
+ it('returns proper URL when screen is narrow', () => {
+ const url = getTestDriveURL(true, undefined, true);
+
+ expect(url).toBe(CONST.STORYLANE.ADMIN_TOUR_MOBILE);
+ });
+
+ it('returns proper URL when screen is not narrow', () => {
+ const url = getTestDriveURL(false, undefined, true);
- expect(url).toBe(CONST.STORYLANE.ADMIN_TOUR_MOBILE);
+ expect(url).toBe(CONST.STORYLANE.ADMIN_TOUR);
+ });
});
- it('returns proper URL when screen is not narrow', () => {
- const url = getTestDriveURL(false);
+ describe('Default case - Employee tour', () => {
+ it('returns proper URL when screen is narrow', () => {
+ const url = getTestDriveURL(true, undefined, false);
+
+ expect(url).toBe(CONST.STORYLANE.EMPLOYEE_TOUR_MOBILE);
+ });
+
+ it('returns proper URL when screen is not narrow', () => {
+ const url = getTestDriveURL(false, undefined, false);
- expect(url).toBe(CONST.STORYLANE.ADMIN_TOUR);
+ expect(url).toBe(CONST.STORYLANE.EMPLOYEE_TOUR);
+ });
});
});
});