From c5870964c653ef65866efd27bdd0555f23ff2034 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Fri, 11 Apr 2025 03:45:25 +0100 Subject: [PATCH 1/6] feat(testdrive): show test drive modal after onboarding --- src/ROUTES.ts | 1 + src/libs/navigateAfterOnboarding.ts | 11 ++++++++++ .../BaseOnboardingAccounting.tsx | 1 + .../BaseOnboardingPersonalDetails.tsx | 2 +- .../BaseOnboardingWorkspaces.tsx | 2 +- tests/unit/navigateAfterOnboardingTest.ts | 20 +++++++++++++------ 6 files changed, 29 insertions(+), 8 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 9bf7d0d650090..98b9aafe3c736 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1845,6 +1845,7 @@ const ROUTES = { WELCOME_VIDEO_ROOT: 'onboarding/welcome-video', EXPLANATION_MODAL_ROOT: 'onboarding/explanation', TEST_DRIVE_MODAL_ROOT: 'onboarding/test-drive', + TEST_DRIVE_DEMO_ROOT: 'onboarding/test-drive/demo', WORKSPACE_CONFIRMATION: { route: 'workspace/confirmation', getRoute: (backTo?: string) => getUrlWithBackToParam(`workspace/confirmation`, backTo), diff --git a/src/libs/navigateAfterOnboarding.ts b/src/libs/navigateAfterOnboarding.ts index e7e31a5933965..d3ba08627a96a 100644 --- a/src/libs/navigateAfterOnboarding.ts +++ b/src/libs/navigateAfterOnboarding.ts @@ -1,9 +1,13 @@ +import {InteractionManager} from 'react-native'; +import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; +import type {OnboardingPurpose} from '@src/types/onyx'; import shouldOpenOnAdminRoom from './Navigation/helpers/shouldOpenOnAdminRoom'; import Navigation from './Navigation/Navigation'; import {findLastAccessedReport, isConciergeChatReport} from './ReportUtils'; const navigateAfterOnboarding = ( + onboardingPurposeSelected: OnboardingPurpose, isSmallScreenWidth: boolean, canUseDefaultRooms: boolean | undefined, onboardingPolicyID?: string, @@ -13,6 +17,13 @@ const navigateAfterOnboarding = ( ) => { Navigation.dismissModal(); + if (onboardingPurposeSelected === CONST.ONBOARDING_CHOICES.MANAGE_TEAM) { + InteractionManager.runAfterInteractions(() => { + Navigation.navigate(ROUTES.TEST_DRIVE_MODAL_ROOT); + }); + return; + } + // When hasCompletedGuidedSetupFlow is true, OnboardingModalNavigator in AuthScreen is removed from the navigation stack. // On small screens, this removal redirects navigation to HOME. Dismissing the modal doesn't work properly, // so we need to specifically navigate to the last accessed report. diff --git a/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx b/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx index 5987cf7737271..ef6aac8aa973a 100644 --- a/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx +++ b/src/pages/OnboardingAccounting/BaseOnboardingAccounting.tsx @@ -186,6 +186,7 @@ function BaseOnboardingAccounting({shouldUseNativeStyles}: BaseOnboardingAccount setOnboardingPolicyID(); }); navigateAfterOnboarding( + onboardingPurposeSelected, isSmallScreenWidth, canUseDefaultRooms, onboardingPolicyID, diff --git a/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx b/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx index 88d65bf76efb9..0a296b49366b2 100644 --- a/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx +++ b/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx @@ -70,7 +70,7 @@ function BaseOnboardingPersonalDetails({currentUserPersonalDetails, shouldUseNat setOnboardingAdminsChatReportID(); setOnboardingPolicyID(); - navigateAfterOnboarding(isSmallScreenWidth, canUseDefaultRooms, onboardingPolicyID, activeWorkspaceID); + navigateAfterOnboarding(onboardingPurposeSelected, isSmallScreenWidth, canUseDefaultRooms, onboardingPolicyID, activeWorkspaceID); }, [onboardingPurposeSelected, onboardingAdminsChatReportID, onboardingPolicyID, activeWorkspaceID, canUseDefaultRooms, isSmallScreenWidth], ); diff --git a/src/pages/OnboardingWorkspaces/BaseOnboardingWorkspaces.tsx b/src/pages/OnboardingWorkspaces/BaseOnboardingWorkspaces.tsx index 2137cccf9c868..3eaab08da5da6 100644 --- a/src/pages/OnboardingWorkspaces/BaseOnboardingWorkspaces.tsx +++ b/src/pages/OnboardingWorkspaces/BaseOnboardingWorkspaces.tsx @@ -58,7 +58,7 @@ function BaseOnboardingWorkspaces({shouldUseNativeStyles, route}: BaseOnboarding setOnboardingAdminsChatReportID(); setOnboardingPolicyID(policyID); - navigateAfterOnboarding(isSmallScreenWidth, canUseDefaultRooms, policyID, activeWorkspaceID); + navigateAfterOnboarding(CONST.ONBOARDING_CHOICES.LOOKING_AROUND, isSmallScreenWidth, canUseDefaultRooms, policyID, activeWorkspaceID); }, [onboardingPersonalDetails?.firstName, onboardingPersonalDetails?.lastName, isSmallScreenWidth, canUseDefaultRooms, activeWorkspaceID], ); diff --git a/tests/unit/navigateAfterOnboardingTest.ts b/tests/unit/navigateAfterOnboardingTest.ts index 85cafb6fc535c..c607e9eb428cf 100644 --- a/tests/unit/navigateAfterOnboardingTest.ts +++ b/tests/unit/navigateAfterOnboardingTest.ts @@ -58,12 +58,12 @@ describe('navigateAfterOnboarding', () => { const navigate = jest.spyOn(Navigation, 'navigate'); const testSession = {email: 'realaccount@gmail.com'}; - navigateAfterOnboarding(false, true, undefined, undefined, ONBOARDING_ADMINS_CHAT_REPORT_ID, (testSession?.email ?? '').includes('+')); + navigateAfterOnboarding(CONST.ONBOARDING_CHOICES.LOOKING_AROUND, false, true, undefined, undefined, ONBOARDING_ADMINS_CHAT_REPORT_ID, (testSession?.email ?? '').includes('+')); expect(navigate).toHaveBeenCalledWith(ROUTES.REPORT_WITH_ID.getRoute(ONBOARDING_ADMINS_CHAT_REPORT_ID)); }); it('should not navigate if onboardingAdminsChatReportID is not provided', () => { - navigateAfterOnboarding(false, true, undefined, undefined); + navigateAfterOnboarding(CONST.ONBOARDING_CHOICES.LOOKING_AROUND, false, true, undefined, undefined); expect(Navigation.navigate).not.toHaveBeenCalled(); }); @@ -82,7 +82,7 @@ describe('navigateAfterOnboarding', () => { mockFindLastAccessedReport.mockReturnValue(lastAccessedReport); mockShouldOpenOnAdminRoom.mockReturnValue(false); - navigateAfterOnboarding(true, true, ONBOARDING_POLICY_ID, ACTIVE_WORKSPACE_ID, ONBOARDING_ADMINS_CHAT_REPORT_ID); + navigateAfterOnboarding(CONST.ONBOARDING_CHOICES.LOOKING_AROUND, true, true, ONBOARDING_POLICY_ID, ACTIVE_WORKSPACE_ID, ONBOARDING_ADMINS_CHAT_REPORT_ID); expect(navigate).not.toHaveBeenCalled(); }); @@ -91,7 +91,7 @@ describe('navigateAfterOnboarding', () => { mockFindLastAccessedReport.mockReturnValue(lastAccessedReport); mockShouldOpenOnAdminRoom.mockReturnValue(false); - navigateAfterOnboarding(true, true, ONBOARDING_POLICY_ID, ACTIVE_WORKSPACE_ID, ONBOARDING_ADMINS_CHAT_REPORT_ID); + navigateAfterOnboarding(CONST.ONBOARDING_CHOICES.LOOKING_AROUND, true, true, ONBOARDING_POLICY_ID, ACTIVE_WORKSPACE_ID, ONBOARDING_ADMINS_CHAT_REPORT_ID); expect(Navigation.navigate).not.toHaveBeenCalled(); }); @@ -101,7 +101,7 @@ describe('navigateAfterOnboarding', () => { mockFindLastAccessedReport.mockReturnValue(lastAccessedReport); mockShouldOpenOnAdminRoom.mockReturnValue(true); - navigateAfterOnboarding(true, true, ONBOARDING_POLICY_ID, ACTIVE_WORKSPACE_ID, ONBOARDING_ADMINS_CHAT_REPORT_ID); + navigateAfterOnboarding(CONST.ONBOARDING_CHOICES.LOOKING_AROUND, true, true, ONBOARDING_POLICY_ID, ACTIVE_WORKSPACE_ID, ONBOARDING_ADMINS_CHAT_REPORT_ID); expect(navigate).toHaveBeenCalledWith(ROUTES.REPORT_WITH_ID.getRoute(REPORT_ID)); }); @@ -112,7 +112,15 @@ describe('navigateAfterOnboarding', () => { mockShouldOpenOnAdminRoom.mockReturnValue(true); const testSession = {email: 'test+account@gmail.com'}; - navigateAfterOnboarding(true, true, ONBOARDING_POLICY_ID, ACTIVE_WORKSPACE_ID, ONBOARDING_ADMINS_CHAT_REPORT_ID, (testSession?.email ?? '').includes('+')); + navigateAfterOnboarding( + CONST.ONBOARDING_CHOICES.LOOKING_AROUND, + true, + true, + ONBOARDING_POLICY_ID, + ACTIVE_WORKSPACE_ID, + ONBOARDING_ADMINS_CHAT_REPORT_ID, + (testSession?.email ?? '').includes('+'), + ); expect(navigate).toHaveBeenCalledWith(ROUTES.REPORT_WITH_ID.getRoute(REPORT_ID)); }); }); From 8604e26325a2ec14bdbdd4a13a3f4064070f0c44 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Fri, 11 Apr 2025 03:46:19 +0100 Subject: [PATCH 2/6] feat(testdrive): replace self guided tour task with test drive --- src/CONST.ts | 25 ++++++++++++++++--------- src/libs/ReportUtils.ts | 5 +++-- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 2fd7c0d2cd565..2373cd7feab73 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -113,12 +113,13 @@ const signupQualifiers = { SMB: 'smb', } as const; -const selfGuidedTourTask: OnboardingTask = { +const getTestDriveTaskName = (testDriveURL?: string) => (testDriveURL ? `Take a [test drive](${testDriveURL})` : 'Take a test drive'); +const testDriveTask: OnboardingTask = { type: 'viewTour', autoCompleted: false, mediaAttributes: {}, - title: ({navatticURL}) => `Take a [2-minute tour](${navatticURL})`, - description: ({navatticURL}) => `[Take a self-guided product tour](${navatticURL}) and learn about everything Expensify has to offer.`, + title: ({testDriveURL}) => getTestDriveTaskName(testDriveURL), + description: ({testDriveURL}) => `[Take a quick product tour](${testDriveURL}) to see why Expensify is the fastest way to do your expenses.`, }; const createWorkspaceTask: OnboardingTask = { @@ -164,7 +165,7 @@ const setupCategoriesTask: OnboardingTask = { const onboardingEmployerOrSubmitMessage: OnboardingMessage = { message: 'Getting paid back is as easy as sending a message. Let’s go over the basics.', tasks: [ - selfGuidedTourTask, + testDriveTask, { type: 'submitExpense', autoCompleted: false, @@ -188,7 +189,7 @@ const onboardingEmployerOrSubmitMessage: OnboardingMessage = { const combinedTrackSubmitOnboardingEmployerOrSubmitMessage: OnboardingMessage = { ...onboardingEmployerOrSubmitMessage, tasks: [ - selfGuidedTourTask, + testDriveTask, { type: 'submitExpense', autoCompleted: false, @@ -213,7 +214,7 @@ const combinedTrackSubmitOnboardingEmployerOrSubmitMessage: OnboardingMessage = const onboardingPersonalSpendMessage: OnboardingMessage = { message: 'Here’s how to track your spend in a few clicks.', tasks: [ - selfGuidedTourTask, + testDriveTask, { type: 'trackExpense', autoCompleted: false, @@ -237,7 +238,7 @@ const onboardingPersonalSpendMessage: OnboardingMessage = { const combinedTrackSubmitOnboardingPersonalSpendMessage: OnboardingMessage = { ...onboardingPersonalSpendMessage, tasks: [ - selfGuidedTourTask, + testDriveTask, { type: 'trackExpense', autoCompleted: false, @@ -289,6 +290,7 @@ type OnboardingTaskLinks = Partial<{ workspaceMembersLink: string; workspaceAccountingLink: string; navatticURL: string; + testDriveURL: string; }>; type OnboardingTask = { @@ -5343,7 +5345,7 @@ const CONST = { message: 'Here are some important tasks to help get your team’s expenses under control.', tasks: [ createWorkspaceTask, - selfGuidedTourTask, + testDriveTask, { type: 'setupCategoriesAndTags', autoCompleted: false, @@ -5477,7 +5479,7 @@ const CONST = { [onboardingChoices.CHAT_SPLIT]: { message: 'Splitting bills with friends is as easy as sending a message. Here’s how.', tasks: [ - selfGuidedTourTask, + testDriveTask, { type: 'startChat', autoCompleted: false, @@ -6939,6 +6941,10 @@ const CONST = { BILLING: { TYPE_FAILED_2018: 'failed_2018', }, + + TEST_DRIVE: { + ONBOARDING_TASK_NAME: getTestDriveTaskName(), + }, } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; @@ -6958,6 +6964,7 @@ export type { IOUType, OnboardingPurpose, OnboardingCompanySize, + OnboardingTaskLinks, IOURequestType, SubscriptionType, FeedbackSurveyOptionID, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 05d6dd8807c86..b8c88c14971bf 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -17,7 +17,7 @@ import {FallbackAvatar, IntacctSquare, NetSuiteSquare, QBOSquare, XeroSquare} fr import * as defaultGroupAvatars from '@components/Icon/GroupDefaultAvatars'; import * as defaultWorkspaceAvatars from '@components/Icon/WorkspaceDefaultAvatars'; import type {MoneyRequestAmountInputProps} from '@components/MoneyRequestAmountInput'; -import type {IOUAction, IOUType, OnboardingAccounting, OnboardingPurpose} from '@src/CONST'; +import type {IOUAction, IOUType, OnboardingAccounting, OnboardingPurpose, OnboardingTaskLinks} from '@src/CONST'; import CONST from '@src/CONST'; import type {ParentNavigationSummaryParams} from '@src/languages/params'; import type {TranslationPaths} from '@src/languages/types'; @@ -9455,13 +9455,14 @@ function prepareOnboardingOnyxData( reportComment: textComment.commentText, }; - const onboardingTaskParams = { + const onboardingTaskParams: OnboardingTaskLinks = { integrationName, workspaceSettingsLink: `${environmentURL}/${ROUTES.WORKSPACE_INITIAL.getRoute(onboardingPolicyID)}`, workspaceCategoriesLink: `${environmentURL}/${ROUTES.WORKSPACE_CATEGORIES.getRoute(onboardingPolicyID)}`, workspaceMembersLink: `${environmentURL}/${ROUTES.WORKSPACE_MEMBERS.getRoute(onboardingPolicyID)}`, workspaceMoreFeaturesLink: `${environmentURL}/${ROUTES.WORKSPACE_MORE_FEATURES.getRoute(onboardingPolicyID)}`, navatticURL: getNavatticURL(environment, engagementChoice), + testDriveURL: `${environmentURL}/${ROUTES.TEST_DRIVE_DEMO_ROOT}`, workspaceAccountingLink: `${environmentURL}/${ROUTES.POLICY_ACCOUNTING.getRoute(onboardingPolicyID)}`, }; From be7cfcbf04c207a6e0bbf572b8edc87364cdba09 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Fri, 11 Apr 2025 04:24:42 +0100 Subject: [PATCH 3/6] feat(testdrive): implement test drive embedded ui --- src/CONST.ts | 7 +++ src/NAVIGATORS.ts | 1 + src/SCREENS.ts | 4 ++ src/components/EmbeddedDemo/index.native.tsx | 23 +++++++ src/components/EmbeddedDemo/index.tsx | 18 ++++++ src/components/EmbeddedDemo/types.ts | 11 ++++ src/components/TestDrive/TestDriveBanner.tsx | 37 +++++++++++ src/components/TestDrive/TestDriveDemo.tsx | 61 +++++++++++++++++++ src/components/TestDriveModal.tsx | 7 ++- src/languages/en.ts | 5 ++ src/languages/es.ts | 5 ++ .../Navigation/AppNavigator/AuthScreens.tsx | 6 ++ .../AppNavigator/TestDriveDemoNavigator.tsx | 29 +++++++++ src/libs/Navigation/linkingConfig/config.ts | 9 +++ src/libs/Navigation/types.ts | 6 ++ src/libs/TourUtils.ts | 9 +++ src/libs/actions/Task.ts | 22 +++++++ .../utils/generators/ModalStyleUtils.ts | 6 ++ 18 files changed, 265 insertions(+), 1 deletion(-) create mode 100644 src/components/EmbeddedDemo/index.native.tsx create mode 100644 src/components/EmbeddedDemo/index.tsx create mode 100644 src/components/EmbeddedDemo/types.ts create mode 100644 src/components/TestDrive/TestDriveBanner.tsx create mode 100644 src/components/TestDrive/TestDriveDemo.tsx create mode 100644 src/libs/Navigation/AppNavigator/TestDriveDemoNavigator.tsx diff --git a/src/CONST.ts b/src/CONST.ts index 2373cd7feab73..ff54981cac7f1 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1076,6 +1076,12 @@ const CONST = { EMPLOYEE_TOUR_STAGING: 'https://expensify.navattic.com/cf15002s', COMPLETED: 'completed', }, + STORYLANE: { + ADMIN_TOUR_PRODUCTION: 'https://app.storylane.io/demo/nrkhnm80nbix?embed=inline', + ADMIN_TOUR_MOBILE_PRODUCTION: 'https://app.storylane.io/demo/wg7a9qqg6qkf?embed=inline', + ADMIN_TOUR_STAGING: 'https://app.storylane.io/demo/nrkhnm80nbix?embed=inline', + ADMIN_TOUR_MOBILE_STAGING: 'https://app.storylane.io/demo/wg7a9qqg6qkf?embed=inline', + }, OLD_DOT_PUBLIC_URLS: { TERMS_URL: `${EXPENSIFY_URL}/terms`, PRIVACY_URL: `${EXPENSIFY_URL}/privacy`, @@ -1473,6 +1479,7 @@ const CONST = { BOTTOM_DOCKED: 'bottom_docked', POPOVER: 'popover', RIGHT_DOCKED: 'right_docked', + FULLSCREEN: 'fullscreen', }, ANCHOR_ORIGIN_VERTICAL: { TOP: 'top', diff --git a/src/NAVIGATORS.ts b/src/NAVIGATORS.ts index 0974bcf6904c7..178943ce056fc 100644 --- a/src/NAVIGATORS.ts +++ b/src/NAVIGATORS.ts @@ -12,6 +12,7 @@ export default { EXPLANATION_MODAL_NAVIGATOR: 'ExplanationModalNavigator', MIGRATED_USER_MODAL_NAVIGATOR: 'MigratedUserModalNavigator', TEST_DRIVE_MODAL_NAVIGATOR: 'TestDriveModalNavigator', + TEST_DRIVE_DEMO_NAVIGATOR: 'TestDriveDemoNavigator', REPORTS_SPLIT_NAVIGATOR: 'ReportsSplitNavigator', SETTINGS_SPLIT_NAVIGATOR: 'SettingsSplitNavigator', WORKSPACE_SPLIT_NAVIGATOR: 'WorkspaceSplitNavigator', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 73f6fa29add88..6fa5b7641be35 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -650,6 +650,10 @@ const SCREENS = { ROOT: 'TestDrive_Modal_Root', }, + TEST_DRIVE_DEMO: { + ROOT: 'TestDrive_Demo_Root', + }, + I_KNOW_A_TEACHER: 'I_Know_A_Teacher', INTRO_SCHOOL_PRINCIPAL: 'Intro_School_Principal', I_AM_A_TEACHER: 'I_Am_A_Teacher', diff --git a/src/components/EmbeddedDemo/index.native.tsx b/src/components/EmbeddedDemo/index.native.tsx new file mode 100644 index 0000000000000..82eca9bcc8af1 --- /dev/null +++ b/src/components/EmbeddedDemo/index.native.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import WebView from 'react-native-webview'; +import type EmbeddedDemoProps from './types'; + +function EmbeddedDemo({url, webViewProps}: EmbeddedDemoProps) { + return ( + + ); +} + +EmbeddedDemo.displayName = 'EmbeddedDemo'; + +export default EmbeddedDemo; diff --git a/src/components/EmbeddedDemo/index.tsx b/src/components/EmbeddedDemo/index.tsx new file mode 100644 index 0000000000000..0597bdbe12933 --- /dev/null +++ b/src/components/EmbeddedDemo/index.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import type EmbeddedDemoProps from './types'; + +function EmbeddedDemo({url, iframeTitle, iframeProps}: EmbeddedDemoProps) { + return ( +