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); + }); }); }); });