From 1aceecf7d77f7e4c6fcf0fc95a349b3490375269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Tue, 29 Jul 2025 17:40:49 +0100 Subject: [PATCH 1/8] Extract startTestDrive logic to its TourUtils --- src/libs/TourUtils.ts | 25 ++++++++++++++-- src/pages/Search/EmptySearchView.tsx | 29 +++++-------------- .../FloatingActionButtonAndPopover.tsx | 21 ++------------ 3 files changed, 34 insertions(+), 41 deletions(-) diff --git a/src/libs/TourUtils.ts b/src/libs/TourUtils.ts index 1fcf3a6683ee3..ada0ee3b01e56 100644 --- a/src/libs/TourUtils.ts +++ b/src/libs/TourUtils.ts @@ -1,5 +1,11 @@ +import {InteractionManager} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; +import type {Report} from '@src/types/onyx'; import type {IntroSelected} from './actions/Report'; +import {completeTestDriveTask} from './actions/Task'; +import Navigation from './Navigation/Navigation'; function getTestDriveURL(shouldUseNarrowLayout: boolean, introSelected?: IntroSelected) { if (introSelected?.choice === CONST.ONBOARDING_CHOICES.SUBMIT && introSelected.inviteType === CONST.ONBOARDING_INVITE_TYPES.WORKSPACE) { @@ -13,5 +19,20 @@ function getTestDriveURL(shouldUseNarrowLayout: boolean, introSelected?: IntroSe return shouldUseNarrowLayout ? CONST.STORYLANE.ADMIN_TOUR_MOBILE : CONST.STORYLANE.ADMIN_TOUR; } -// eslint-disable-next-line import/prefer-default-export -export {getTestDriveURL}; +function startTestDrive(introSelected: IntroSelected | undefined, viewTourReport: OnyxEntry, viewTourReportID: string | undefined, shouldUpdateSelfTourViewedOnlyLocally = false) { + 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(viewTourReport, viewTourReportID, shouldUpdateSelfTourViewedOnlyLocally); + Navigation.navigate(ROUTES.TEST_DRIVE_DEMO_ROOT); + } else { + Navigation.navigate(ROUTES.TEST_DRIVE_MODAL_ROOT.route); + } + }); +} + +export {getTestDriveURL, startTestDrive}; diff --git a/src/pages/Search/EmptySearchView.tsx b/src/pages/Search/EmptySearchView.tsx index e251fe5d794c3..c73a736426a4b 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,13 +28,13 @@ 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 interceptAnonymousUser from '@libs/interceptAnonymousUser'; import Navigation from '@libs/Navigation/Navigation'; import {hasSeenTourSelector} from '@libs/onboardingSelectors'; import {areAllGroupPoliciesExpenseChatDisabled, getGroupPaidPoliciesWithExpenseChatEnabled, isPaidGroupPolicy} from '@libs/PolicyUtils'; import {generateReportID} from '@libs/ReportUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; +import {startTestDrive} from '@libs/TourUtils'; import {showContextMenu} from '@pages/home/report/ContextMenu/ReportActionContextMenu'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -196,20 +196,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(viewTourReport, viewTourReportID); - Navigation.navigate(ROUTES.TEST_DRIVE_DEMO_ROOT); - } else { - Navigation.navigate(ROUTES.TEST_DRIVE_MODAL_ROOT.route); - } - }); + const startTestDriveAction = () => { + startTestDrive(introSelected, viewTourReport, viewTourReportID); }; // If we are grouping by reports, show a custom message rather than a type-specific message @@ -231,7 +219,7 @@ function EmptySearchView({hash, type, groupBy, hasResults}: EmptySearchViewProps ? [ { buttonText: translate('emptySearchView.takeATestDrive'), - buttonAction: startTestDrive, + buttonAction: startTestDriveAction, }, ] : []), @@ -298,7 +286,7 @@ function EmptySearchView({hash, type, groupBy, hasResults}: EmptySearchViewProps ? [ { buttonText: translate('emptySearchView.takeATestDrive'), - buttonAction: startTestDrive, + buttonAction: startTestDriveAction, }, ] : []), @@ -329,7 +317,7 @@ function EmptySearchView({hash, type, groupBy, hasResults}: EmptySearchViewProps ? [ { buttonText: translate('emptySearchView.takeATestDrive'), - buttonAction: startTestDrive, + buttonAction: startTestDriveAction, }, ] : []), @@ -371,8 +359,7 @@ function EmptySearchView({hash, type, groupBy, hasResults}: EmptySearchViewProps styles.emptyStateFolderWebStyles, styles.textAlignLeft, styles.tripEmptyStateLottieWebView, - introSelected?.choice, - introSelected?.inviteType, + introSelected, hasResults, defaultViewItemHeader, hasSeenTour, diff --git a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx index 517b381638659..f0932d4c73f18 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'; @@ -26,7 +26,6 @@ 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 getIconForAction from '@libs/getIconForAction'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; @@ -44,6 +43,7 @@ import { import {getQuickActionIcon, getQuickActionTitle, isQuickActionAllowed} from '@libs/QuickActionUtils'; import {generateReportID, getDisplayNameForParticipant, getIcons, getReportName, getWorkspaceChats, isArchivedReport, isPolicyExpenseChat} from '@libs/ReportUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; +import {startTestDrive} from '@libs/TourUtils'; import variables from '@styles/variables'; import closeReactNativeApp from '@userActions/HybridApp'; import CONFIG from '@src/CONFIG'; @@ -526,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(viewTourReport, viewTourReportID, isAnonymousUser()); - Navigation.navigate(ROUTES.TEST_DRIVE_DEMO_ROOT); - } else { - Navigation.navigate(ROUTES.TEST_DRIVE_MODAL_ROOT.route); - } - }); - }), + onSelected: () => interceptAnonymousUser(() => startTestDrive(introSelected, viewTourReport, viewTourReportID, isAnonymousUser())), }, ] : []), From 8c8ed9fda894a2273def3632808dcd72883be01c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Tue, 29 Jul 2025 17:49:28 +0100 Subject: [PATCH 2/8] Show the TestDrive demo to users if they migrated from Classic --- src/libs/TourUtils.ts | 11 +++++++++-- src/pages/Search/EmptySearchView.tsx | 6 ++++-- .../home/sidebar/FloatingActionButtonAndPopover.tsx | 6 ++++-- 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/libs/TourUtils.ts b/src/libs/TourUtils.ts index ada0ee3b01e56..cb11e1aaa049f 100644 --- a/src/libs/TourUtils.ts +++ b/src/libs/TourUtils.ts @@ -7,7 +7,7 @@ import type {IntroSelected} from './actions/Report'; import {completeTestDriveTask} from './actions/Task'; import Navigation from './Navigation/Navigation'; -function getTestDriveURL(shouldUseNarrowLayout: boolean, introSelected?: IntroSelected) { +function getTestDriveURL(shouldUseNarrowLayout: boolean, introSelected?: IntroSelected): string { 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; } @@ -19,9 +19,16 @@ function getTestDriveURL(shouldUseNarrowLayout: boolean, introSelected?: IntroSe return shouldUseNarrowLayout ? CONST.STORYLANE.ADMIN_TOUR_MOBILE : CONST.STORYLANE.ADMIN_TOUR; } -function startTestDrive(introSelected: IntroSelected | undefined, viewTourReport: OnyxEntry, viewTourReportID: string | undefined, shouldUpdateSelfTourViewedOnlyLocally = false) { +function startTestDrive( + introSelected: IntroSelected | undefined, + viewTourReport: OnyxEntry, + viewTourReportID: string | 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 || diff --git a/src/pages/Search/EmptySearchView.tsx b/src/pages/Search/EmptySearchView.tsx index c73a736426a4b..e05e20eaf4793 100644 --- a/src/pages/Search/EmptySearchView.tsx +++ b/src/pages/Search/EmptySearchView.tsx @@ -30,7 +30,7 @@ import {openOldDotLink} from '@libs/actions/Link'; import {createNewReport} from '@libs/actions/Report'; 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(); @@ -197,7 +198,7 @@ function EmptySearchView({hash, type, groupBy, hasResults}: EmptySearchViewProps } const startTestDriveAction = () => { - startTestDrive(introSelected, viewTourReport, viewTourReportID); + startTestDrive(introSelected, viewTourReport, viewTourReportID, false, tryNewDot?.hasBeenAddedToNudgeMigration); }; // If we are grouping by reports, show a custom message rather than a type-specific message @@ -372,6 +373,7 @@ function EmptySearchView({hash, type, groupBy, hasResults}: EmptySearchViewProps viewTourReport, viewTourReportID, transactions, + tryNewDot?.hasBeenAddedToNudgeMigration, ]); return ( diff --git a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx index f0932d4c73f18..aba92f80d5017 100644 --- a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx @@ -31,7 +31,7 @@ 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, @@ -136,6 +136,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, isT }); const viewTourReportID = introSelected?.viewTour; const [viewTourReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${viewTourReportID}`, {canBeMissing: true}); + const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT, {selector: tryNewDotOnyxSelector, canBeMissing: true}); const groupPoliciesWithChatEnabled = getGroupPaidPoliciesWithExpenseChatEnabled(); @@ -526,7 +527,8 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, isT iconStyles: styles.popoverIconCircle, iconFill: theme.icon, text: translate('testDrive.quickAction.takeATwoMinuteTestDrive'), - onSelected: () => interceptAnonymousUser(() => startTestDrive(introSelected, viewTourReport, viewTourReportID, isAnonymousUser())), + onSelected: () => + interceptAnonymousUser(() => startTestDrive(introSelected, viewTourReport, viewTourReportID, isAnonymousUser(), tryNewDot?.hasBeenAddedToNudgeMigration)), }, ] : []), From d15d5d90d61d6d7d2a8e605515a719d91b477359 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Tue, 29 Jul 2025 18:56:44 +0100 Subject: [PATCH 3/8] Move startTestDrive function to action file --- src/libs/TourUtils.ts | 32 ++--------------- src/libs/actions/Tour.ts | 34 +++++++++++++++++++ src/pages/Search/EmptySearchView.tsx | 2 +- .../FloatingActionButtonAndPopover.tsx | 2 +- 4 files changed, 38 insertions(+), 32 deletions(-) create mode 100644 src/libs/actions/Tour.ts diff --git a/src/libs/TourUtils.ts b/src/libs/TourUtils.ts index cb11e1aaa049f..a96915e46481f 100644 --- a/src/libs/TourUtils.ts +++ b/src/libs/TourUtils.ts @@ -1,11 +1,5 @@ -import {InteractionManager} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; -import ROUTES from '@src/ROUTES'; -import type {Report} from '@src/types/onyx'; import type {IntroSelected} from './actions/Report'; -import {completeTestDriveTask} from './actions/Task'; -import Navigation from './Navigation/Navigation'; function getTestDriveURL(shouldUseNarrowLayout: boolean, introSelected?: IntroSelected): string { if (introSelected?.choice === CONST.ONBOARDING_CHOICES.SUBMIT && introSelected.inviteType === CONST.ONBOARDING_INVITE_TYPES.WORKSPACE) { @@ -19,27 +13,5 @@ function getTestDriveURL(shouldUseNarrowLayout: boolean, introSelected?: IntroSe return shouldUseNarrowLayout ? CONST.STORYLANE.ADMIN_TOUR_MOBILE : CONST.STORYLANE.ADMIN_TOUR; } -function startTestDrive( - introSelected: IntroSelected | undefined, - viewTourReport: OnyxEntry, - viewTourReportID: string | 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(viewTourReport, viewTourReportID, shouldUpdateSelfTourViewedOnlyLocally); - Navigation.navigate(ROUTES.TEST_DRIVE_DEMO_ROOT); - } else { - Navigation.navigate(ROUTES.TEST_DRIVE_MODAL_ROOT.route); - } - }); -} - -export {getTestDriveURL, startTestDrive}; +// eslint-disable-next-line import/prefer-default-export +export {getTestDriveURL}; diff --git a/src/libs/actions/Tour.ts b/src/libs/actions/Tour.ts new file mode 100644 index 0000000000000..3102739ad1430 --- /dev/null +++ b/src/libs/actions/Tour.ts @@ -0,0 +1,34 @@ +import {InteractionManager} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import Navigation from '@libs/Navigation/Navigation'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; +import type {Report} from '@src/types/onyx'; +import type {IntroSelected} from './Report'; +import {completeTestDriveTask} from './Task'; + +function startTestDrive( + introSelected: IntroSelected | undefined, + viewTourReport: OnyxEntry, + viewTourReportID: string | 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(viewTourReport, viewTourReportID, 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 e05e20eaf4793..f2c29438e27f9 100644 --- a/src/pages/Search/EmptySearchView.tsx +++ b/src/pages/Search/EmptySearchView.tsx @@ -28,13 +28,13 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {startMoneyRequest} from '@libs/actions/IOU'; import {openOldDotLink} from '@libs/actions/Link'; import {createNewReport} from '@libs/actions/Report'; +import {startTestDrive} from '@libs/actions/Tour'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import Navigation from '@libs/Navigation/Navigation'; import {hasSeenTourSelector, tryNewDotOnyxSelector} from '@libs/onboardingSelectors'; import {areAllGroupPoliciesExpenseChatDisabled, getGroupPaidPoliciesWithExpenseChatEnabled, isPaidGroupPolicy} from '@libs/PolicyUtils'; import {generateReportID} from '@libs/ReportUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; -import {startTestDrive} from '@libs/TourUtils'; import {showContextMenu} from '@pages/home/report/ContextMenu/ReportActionContextMenu'; import variables from '@styles/variables'; import CONST from '@src/CONST'; diff --git a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx index aba92f80d5017..fcc7ec5b36b4b 100644 --- a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx @@ -26,6 +26,7 @@ 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 {startTestDrive} from '@libs/actions/Tour'; import getIconForAction from '@libs/getIconForAction'; import interceptAnonymousUser from '@libs/interceptAnonymousUser'; import isSearchTopmostFullScreenRoute from '@libs/Navigation/helpers/isSearchTopmostFullScreenRoute'; @@ -43,7 +44,6 @@ import { import {getQuickActionIcon, getQuickActionTitle, isQuickActionAllowed} from '@libs/QuickActionUtils'; import {generateReportID, getDisplayNameForParticipant, getIcons, getReportName, getWorkspaceChats, isArchivedReport, isPolicyExpenseChat} from '@libs/ReportUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; -import {startTestDrive} from '@libs/TourUtils'; import variables from '@styles/variables'; import closeReactNativeApp from '@userActions/HybridApp'; import CONFIG from '@src/CONFIG'; From d7b3653c118a98da549bb5151a5f60b2fd071dde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Wed, 30 Jul 2025 17:23:36 +0100 Subject: [PATCH 4/8] Check if user is admin of workspace when is migrated user --- src/components/TestDrive/TestDriveDemo.tsx | 9 ++- src/libs/TourUtils.ts | 23 +++++-- tests/unit/TourUtilsTest.ts | 78 +++++++++++++++------- 3 files changed, 80 insertions(+), 30 deletions(-) diff --git a/src/components/TestDrive/TestDriveDemo.tsx b/src/components/TestDrive/TestDriveDemo.tsx index 45171c8413c2d..1c7b046ab479e 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'; @@ -27,6 +29,11 @@ function TestDriveDemo() { const viewTourReportID = introSelected?.viewTour; const [viewTourReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${viewTourReportID}`, {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(() => { @@ -64,7 +71,7 @@ function TestDriveDemo() { diff --git a/src/libs/TourUtils.ts b/src/libs/TourUtils.ts index a96915e46481f..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): string { - 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/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); + }); }); }); }); From baf0ad1ef4cb91e3057cfd30eea0d00a1ef9a0d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Wed, 30 Jul 2025 19:22:22 +0100 Subject: [PATCH 5/8] Fix isUserPolicyAdmin function --- src/libs/PolicyUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 42b0663bf5b2d..da71dd4cbe01c 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -312,7 +312,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. From caa6b296ea97a2ab6efac0c30ec6c74e88871af9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Wed, 6 Aug 2025 15:11:57 +0100 Subject: [PATCH 6/8] Fix duplicate migrated user modal --- src/hooks/useOnboardingFlow.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) 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; } From 26154a13bc6bc0220f22cdb8b35e402c38f9fa8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Wed, 6 Aug 2025 15:29:44 +0100 Subject: [PATCH 7/8] Fix TS --- src/libs/actions/Tour.ts | 6 +----- src/pages/Search/EmptySearchView.tsx | 6 +----- src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx | 4 +--- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/src/libs/actions/Tour.ts b/src/libs/actions/Tour.ts index 3102739ad1430..5d7270b031e7b 100644 --- a/src/libs/actions/Tour.ts +++ b/src/libs/actions/Tour.ts @@ -1,16 +1,12 @@ import {InteractionManager} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -import type {Report} from '@src/types/onyx'; import type {IntroSelected} from './Report'; import {completeTestDriveTask} from './Task'; function startTestDrive( introSelected: IntroSelected | undefined, - viewTourReport: OnyxEntry, - viewTourReportID: string | undefined, shouldUpdateSelfTourViewedOnlyLocally = false, hasUserBeenAddedToNudgeMigration = false, ) { @@ -22,7 +18,7 @@ function startTestDrive( introSelected?.choice === CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE || (introSelected?.choice === CONST.ONBOARDING_CHOICES.SUBMIT && introSelected.inviteType === CONST.ONBOARDING_INVITE_TYPES.WORKSPACE) ) { - completeTestDriveTask(viewTourReport, viewTourReportID, shouldUpdateSelfTourViewedOnlyLocally); + completeTestDriveTask(shouldUpdateSelfTourViewedOnlyLocally); Navigation.navigate(ROUTES.TEST_DRIVE_DEMO_ROOT); } else { Navigation.navigate(ROUTES.TEST_DRIVE_MODAL_ROOT.route); diff --git a/src/pages/Search/EmptySearchView.tsx b/src/pages/Search/EmptySearchView.tsx index 85a8f548036c5..1955602e6e911 100644 --- a/src/pages/Search/EmptySearchView.tsx +++ b/src/pages/Search/EmptySearchView.tsx @@ -164,8 +164,6 @@ function EmptySearchView({hash, type, groupBy, hasResults}: EmptySearchViewProps selector: hasSeenTourSelector, canBeMissing: true, }); - const viewTourReportID = introSelected?.viewTour; - const [viewTourReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${viewTourReportID}`, {canBeMissing: true}); // Default 'Folder' lottie animation, along with its background styles const defaultViewItemHeader = useMemo( @@ -198,7 +196,7 @@ function EmptySearchView({hash, type, groupBy, hasResults}: EmptySearchViewProps } const startTestDriveAction = () => { - startTestDrive(introSelected, viewTourReport, viewTourReportID, false, tryNewDot?.hasBeenAddedToNudgeMigration); + startTestDrive(introSelected, false, tryNewDot?.hasBeenAddedToNudgeMigration); }; // If we are grouping by reports, show a custom message rather than a type-specific message @@ -371,8 +369,6 @@ function EmptySearchView({hash, type, groupBy, hasResults}: EmptySearchViewProps tripViewChildren, shouldRedirectToExpensifyClassic, transactions, - viewTourReport, - viewTourReportID, tryNewDot?.hasBeenAddedToNudgeMigration, ]); diff --git a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx index 477c72f2b2898..c8379a8c57ea5 100644 --- a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx @@ -135,8 +135,6 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, isT selector: hasSeenTourSelector, canBeMissing: true, }); - const viewTourReportID = introSelected?.viewTour; - const [viewTourReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${viewTourReportID}`, {canBeMissing: true}); const [tryNewDot] = useOnyx(ONYXKEYS.NVP_TRY_NEW_DOT, {selector: tryNewDotOnyxSelector, canBeMissing: true}); const groupPoliciesWithChatEnabled = getGroupPaidPoliciesWithExpenseChatEnabled(); @@ -529,7 +527,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, isT iconFill: theme.icon, text: translate('testDrive.quickAction.takeATwoMinuteTestDrive'), onSelected: () => - interceptAnonymousUser(() => startTestDrive(introSelected, viewTourReport, viewTourReportID, isAnonymousUser(), tryNewDot?.hasBeenAddedToNudgeMigration)), + interceptAnonymousUser(() => startTestDrive(introSelected, isAnonymousUser(), tryNewDot?.hasBeenAddedToNudgeMigration)), }, ] : []), From 9044135925968414f13223929b5ef7ecc3377db1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Henriques?= Date: Wed, 6 Aug 2025 15:39:20 +0100 Subject: [PATCH 8/8] Fix Prettier --- src/libs/actions/Tour.ts | 6 +----- src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx | 3 +-- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/libs/actions/Tour.ts b/src/libs/actions/Tour.ts index 5d7270b031e7b..895b35013cef3 100644 --- a/src/libs/actions/Tour.ts +++ b/src/libs/actions/Tour.ts @@ -5,11 +5,7 @@ import ROUTES from '@src/ROUTES'; import type {IntroSelected} from './Report'; import {completeTestDriveTask} from './Task'; -function startTestDrive( - introSelected: IntroSelected | undefined, - shouldUpdateSelfTourViewedOnlyLocally = false, - hasUserBeenAddedToNudgeMigration = false, -) { +function startTestDrive(introSelected: IntroSelected | undefined, shouldUpdateSelfTourViewedOnlyLocally = false, hasUserBeenAddedToNudgeMigration = false) { InteractionManager.runAfterInteractions(() => { if ( hasUserBeenAddedToNudgeMigration || diff --git a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx index c8379a8c57ea5..c19152bbcd2bd 100644 --- a/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx +++ b/src/pages/home/sidebar/FloatingActionButtonAndPopover.tsx @@ -526,8 +526,7 @@ function FloatingActionButtonAndPopover({onHideCreateMenu, onShowCreateMenu, isT iconStyles: styles.popoverIconCircle, iconFill: theme.icon, text: translate('testDrive.quickAction.takeATwoMinuteTestDrive'), - onSelected: () => - interceptAnonymousUser(() => startTestDrive(introSelected, isAnonymousUser(), tryNewDot?.hasBeenAddedToNudgeMigration)), + onSelected: () => interceptAnonymousUser(() => startTestDrive(introSelected, isAnonymousUser(), tryNewDot?.hasBeenAddedToNudgeMigration)), }, ] : []),