diff --git a/assets/images/fake-test-drive-employee-receipt.jpg b/assets/images/fake-test-drive-employee-receipt.jpg new file mode 100644 index 0000000000000..78be101cdbebc Binary files /dev/null and b/assets/images/fake-test-drive-employee-receipt.jpg differ diff --git a/assets/images/fast-track-cover.jpg b/assets/images/fast-track-cover.jpg deleted file mode 100644 index 01bd4fc89fc35..0000000000000 Binary files a/assets/images/fast-track-cover.jpg and /dev/null differ diff --git a/assets/images/fast-track-cover.png b/assets/images/fast-track-cover.png new file mode 100644 index 0000000000000..48fa720e872d5 Binary files /dev/null and b/assets/images/fast-track-cover.png differ diff --git a/src/CONST.ts b/src/CONST.ts index 65934582b69c7..421685fbc447a 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -99,6 +99,7 @@ const backendOnboardingChoices = { ADMIN: 'newDotAdmin', SUBMIT: 'newDotSubmit', TRACK_WORKSPACE: 'newDotTrackWorkspace', + TEST_DRIVE_RECEIVER: 'testDriveReceiver', } as const; const onboardingChoices = { @@ -118,21 +119,27 @@ const signupQualifiers = { SMB: 'smb', } as const; -const selfGuidedTourTask: OnboardingTask = { +const getTestDriveTaskName = (testDriveURL?: string) => (testDriveURL ? `Take a [test drive](${testDriveURL})` : 'Take a test drive'); +const testDriveAdminTask: 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 getTestDriveTaskName = (testDriveURL?: string) => (testDriveURL ? `Take a [test drive](${testDriveURL})` : 'Take a test drive'); -const testDriveTask: OnboardingTask = { +const testDriveEmployeeTask: OnboardingTask = { type: 'viewTour', autoCompleted: false, mediaAttributes: {}, title: ({testDriveURL}) => getTestDriveTaskName(testDriveURL), - description: ({testDriveURL}) => `[Take a quick product tour](${testDriveURL}) to see why Expensify is the fastest way to do your expenses.`, + description: ({testDriveURL}) => `Take us for a [test drive](${testDriveURL}) and get your team *3 free months of Expensify!*`, +}; +const createTestDriveAdminWorkspaceTask: OnboardingTask = { + type: 'createWorkspace', + autoCompleted: false, + mediaAttributes: {}, + title: ({workspaceConfirmationLink}) => `[Create](${workspaceConfirmationLink}) a workspace`, + description: 'Create a workspace and configure the settings with the help of your setup specialist!', }; const createWorkspaceTask: OnboardingTask = { @@ -178,7 +185,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, + testDriveEmployeeTask, { type: 'submitExpense', autoCompleted: false, @@ -202,7 +209,7 @@ const onboardingEmployerOrSubmitMessage: OnboardingMessage = { const combinedTrackSubmitOnboardingEmployerOrSubmitMessage: OnboardingMessage = { ...onboardingEmployerOrSubmitMessage, tasks: [ - selfGuidedTourTask, + testDriveEmployeeTask, { type: 'submitExpense', autoCompleted: false, @@ -227,7 +234,7 @@ const combinedTrackSubmitOnboardingEmployerOrSubmitMessage: OnboardingMessage = const onboardingPersonalSpendMessage: OnboardingMessage = { message: 'Here’s how to track your spend in a few clicks.', tasks: [ - selfGuidedTourTask, + testDriveEmployeeTask, { type: 'trackExpense', autoCompleted: false, @@ -251,7 +258,7 @@ const onboardingPersonalSpendMessage: OnboardingMessage = { const combinedTrackSubmitOnboardingPersonalSpendMessage: OnboardingMessage = { ...onboardingPersonalSpendMessage, tasks: [ - selfGuidedTourTask, + testDriveEmployeeTask, { type: 'trackExpense', autoCompleted: false, @@ -303,6 +310,7 @@ type OnboardingTaskLinks = Partial<{ workspaceMoreFeaturesLink: string; workspaceMembersLink: string; workspaceAccountingLink: string; + workspaceConfirmationLink: string; navatticURL: string; testDriveURL: string; corporateCardLink: string; @@ -5240,7 +5248,7 @@ const CONST = { VIDEO_URL: `${CLOUDFRONT_URL}/videos/guided-setup-track-business-v2.mp4`, LEARN_MORE_LINK: `${USE_EXPENSIFY_URL}/track-expenses`, }, - TEST_DRIVE_COVER_ASPECT_RATIO: 500 / 300, + TEST_DRIVE_COVER_ASPECT_RATIO: 1000 / 508, }, /** @@ -5457,7 +5465,7 @@ const CONST = { message: ({onboardingCompanySize: companySize}) => `Here is a task list I’d recommend for a company of your size with ${companySize} submitters:`, tasks: [ createWorkspaceTask, - testDriveTask, + testDriveAdminTask, { type: 'addAccountingIntegration', autoCompleted: false, @@ -5608,7 +5616,7 @@ const CONST = { [onboardingChoices.CHAT_SPLIT]: { message: 'Splitting bills with friends is as easy as sending a message. Here’s how.', tasks: [ - selfGuidedTourTask, + testDriveEmployeeTask, { type: 'startChat', autoCompleted: false, @@ -5685,6 +5693,10 @@ const CONST = { "Expensify is best known for expense and corporate card management, but we do a lot more than that. Let me know what you're interested in and I'll help get you started.", tasks: [], }, + [onboardingChoices.TEST_DRIVE_RECEIVER]: { + message: "*You've got 3 months free! Get started below.*", + tasks: [testDriveAdminTask, createTestDriveAdminWorkspaceTask], + }, } satisfies Record, CREATE_EXPENSE_ONBOARDING_MESSAGES: { @@ -7074,6 +7086,7 @@ const CONST = { GBR_RBR_CHAT: 'chatGBRRBR', ACCOUNT_SWITCHER: 'accountSwitcher', EXPENSE_REPORTS_FILTER: 'expenseReportsFilter', + SCAN_TEST_DRIVE_CONFIRMATION: 'scanTestDriveConfirmation', }, CHANGE_POLICY_TRAINING_MODAL: 'changePolicyModal', SMART_BANNER_HEIGHT: 152, @@ -7123,6 +7136,12 @@ const CONST = { ONBOARDING_TASK_NAME: getTestDriveTaskName(), EMBEDDED_DEMO_WHITELIST: ['http://', 'https://', 'about:'] as string[], EMBEDDED_DEMO_IFRAME_TITLE: 'Test Drive', + EMPLOYEE_FAKE_RECEIPT: { + AMOUNT: 2000, + CURRENCY: 'USD', + DESCRIPTION: 'My test drive receipt!', + MERCHANT: "Tommy's Tires", + }, }, SCHEDULE_CALL_STATUS: { @@ -7157,6 +7176,7 @@ export type { OnboardingInvite, OnboardingAccounting, IOUActionParams, + OnboardingMessage, }; export {getTestDriveTaskName}; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index b8616a19f2901..35d95e4dd9fde 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1963,7 +1963,10 @@ const ROUTES = { }, WELCOME_VIDEO_ROOT: 'onboarding/welcome-video', EXPLANATION_MODAL_ROOT: 'onboarding/explanation', - TEST_DRIVE_MODAL_ROOT: 'onboarding/test-drive', + TEST_DRIVE_MODAL_ROOT: { + route: 'onboarding/test-drive', + getRoute: (bossEmail?: string) => `onboarding/test-drive${bossEmail ? `?bossEmail=${encodeURIComponent(bossEmail)}` : ''}` as const, + }, TEST_DRIVE_DEMO_ROOT: 'onboarding/test-drive/demo', WORKSPACE_CONFIRMATION: { route: 'workspace/confirmation', diff --git a/src/components/FeatureTrainingModal.tsx b/src/components/FeatureTrainingModal.tsx index aba64923917fc..11a5a0639d3a6 100644 --- a/src/components/FeatureTrainingModal.tsx +++ b/src/components/FeatureTrainingModal.tsx @@ -18,11 +18,15 @@ import CONST from '@src/CONST'; import type IconAsset from '@src/types/utils/IconAsset'; import Button from './Button'; import CheckboxWithLabel from './CheckboxWithLabel'; +import FormAlertWithSubmitButton from './FormAlertWithSubmitButton'; import ImageSVG from './ImageSVG'; import Lottie from './Lottie'; import LottieAnimations from './LottieAnimations'; import type DotLottieAnimation from './LottieAnimations/types'; import Modal from './Modal'; +import OfflineIndicator from './OfflineIndicator'; +import RenderHTML from './RenderHTML'; +import ScrollView from './ScrollView'; import Text from './Text'; import VideoPlayer from './VideoPlayer'; @@ -98,6 +102,24 @@ type BaseFeatureTrainingModalProps = { /** Whether the modal image is a SVG */ shouldRenderSVG?: boolean; + + /** Whether the modal description is written in HTML */ + shouldRenderHTMLDescription?: boolean; + + /** Whether the modal will be closed on confirm */ + shouldCloseOnConfirm?: boolean; + + /** Whether the modal should avoid the keyboard */ + avoidKeyboard?: boolean; + + /** Whether the modal content is scrollable */ + shouldUseScrollView?: boolean; + + /** Whether the modal is displaying a confirmation loading spinner (useful when fetching data from API during confirmation) */ + shouldShowConfirmationLoader?: boolean; + + /** Whether the user can confirm the tutorial while offline */ + canConfirmWhileOffline?: boolean; }; type FeatureTrainingModalVideoProps = { @@ -155,6 +177,12 @@ function FeatureTrainingModal({ imageHeight, isModalDisabled = true, shouldRenderSVG = true, + shouldRenderHTMLDescription = false, + shouldCloseOnConfirm = true, + avoidKeyboard = false, + shouldUseScrollView = false, + shouldShowConfirmationLoader = false, + canConfirmWhileOffline = true, }: FeatureTrainingModalProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -297,9 +325,11 @@ function FeatureTrainingModal({ }, [onClose, willShowAgain]); const closeAndConfirmModal = useCallback(() => { - closeModal(); + if (shouldCloseOnConfirm) { + closeModal(); + } onConfirm?.(); - }, [onConfirm, closeModal]); + }, [shouldCloseOnConfirm, onConfirm, closeModal]); /** * Extracts values from the non-scraped attribute WEB_PROP_ATTR at build time @@ -309,14 +339,17 @@ function FeatureTrainingModal({ */ useLayoutEffect(parseFSAttributes, []); + const Wrapper = shouldUseScrollView ? ScrollView : View; + return ( - @@ -341,7 +375,13 @@ function FeatureTrainingModal({ {!!title && !!description && ( {typeof title === 'string' ? {title} : title} - {description} + {shouldRenderHTMLDescription ? ( + + + + ) : ( + {description} + )} {secondaryDescription.length > 0 && {secondaryDescription}} {children} @@ -363,17 +403,19 @@ function FeatureTrainingModal({ text={helpText} /> )} -