diff --git a/assets/images/simple-illustrations/simple-illustration__abacus.svg b/assets/images/simple-illustrations/simple-illustration__abacus.svg new file mode 100644 index 000000000000..df94ab653982 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__abacus.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__binoculars.svg b/assets/images/simple-illustrations/simple-illustration__binoculars.svg new file mode 100644 index 000000000000..381be8988873 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__binoculars.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__company-card.svg b/assets/images/simple-illustrations/simple-illustration__company-card.svg new file mode 100644 index 000000000000..4121bbeeb205 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__company-card.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__piggybank.svg b/assets/images/simple-illustrations/simple-illustration__piggybank.svg new file mode 100644 index 000000000000..a9cf2b02c5dc --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__piggybank.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__receiptupload.svg b/assets/images/simple-illustrations/simple-illustration__receiptupload.svg new file mode 100644 index 000000000000..b8fe5101715f --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__receiptupload.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + diff --git a/assets/images/simple-illustrations/simple-illustration__splitbill.svg b/assets/images/simple-illustrations/simple-illustration__splitbill.svg new file mode 100644 index 000000000000..dfed7535ee90 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__splitbill.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/CONST.ts b/src/CONST.ts index d33cf174cf48..6d1195ff5c79 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -56,6 +56,15 @@ const chatTypes = { // Explicit type annotation is required const cardActiveStates: number[] = [2, 3, 4, 7]; +const onboardingChoices = { + TRACK: 'newDotTrack', + EMPLOYER: 'newDotEmployer', + MANAGE_TEAM: 'newDotManageTeam', + PERSONAL_SPEND: 'newDotPersonalSpend', + CHAT_SPLIT: 'newDotSplitChat', + LOOKING_AROUND: 'newDotLookingAround', +}; + const CONST = { MERGED_ACCOUNT_PREFIX: 'MERGED_', DEFAULT_POLICY_ROOM_CHAT_TYPES: [chatTypes.POLICY_ADMINS, chatTypes.POLICY_ANNOUNCE, chatTypes.DOMAIN_ALL], @@ -842,6 +851,7 @@ const CONST = { BOTTOM_DOCKED: 'bottom_docked', POPOVER: 'popover', RIGHT_DOCKED: 'right_docked', + ONBOARDING: 'onboarding', }, ANCHOR_ORIGIN_VERTICAL: { TOP: 'top', @@ -3429,6 +3439,11 @@ const CONST = { HIDE_TIME_TEXT_WIDTH: 250, MIN_WIDTH: 170, MIN_HEIGHT: 120, + CONTROLS_STATUS: { + SHOW: 'show', + HIDE: 'hide', + VOLUME_ONLY: 'volumeOnly', + }, CONTROLS_POSITION: { NATIVE: 32, NORMAL: 8, @@ -3459,6 +3474,73 @@ const CONST = { MINIMUM_WORKSPACES_TO_SHOW_SEARCH: 8, }, + WELCOME_VIDEO_URL: `${CLOUDFRONT_URL}/videos/intro-1280.mp4`, + + ONBOARDING_CHOICES: {...onboardingChoices}, + + ONBOARDING_CONCIERGE: { + [onboardingChoices.TRACK]: + "# Welcome to Expensify, let's start tracking your expenses!\n" + + "Hi there, I'm Concierge. Chat with me here for anything you need.\n" + + '\n' + + "To track your expenses, create a workspace to keep everything in one place. Here's how:\n" + + '1. From the home screen, click the green + button > New Workspace\n' + + '2. Give your workspace a name (e.g. "My business expenses”).\n' + + '\n' + + 'Then, add expenses to your workspace:\n' + + '1. Find your workspace using the search field.\n' + + '2. Click the gray + button next to the message field.\n' + + '3. Click Request money, then add your expense type.\n' + + '\n' + + "We'll store all expenses in your new workspace for easy access. Let me know if you have any questions!", + [onboardingChoices.EMPLOYER]: + '# Welcome to Expensify, the fastest way to get paid back!\n' + + "Hi there, I'm Concierge. Chat with me here for anything you need.\n" + + '\n' + + 'To submit expenses for reimbursement:\n' + + '1. From the home screen, click the green + button > Request money.\n' + + "2. Enter an amount or scan a receipt, then input your boss's email.\n" + + '\n' + + "That'll send a request to get you paid back. Let me know if you have any questions!", + [onboardingChoices.MANAGE_TEAM]: + "# Welcome to Expensify, let's start managing your team's expenses!\n" + + "Hi there, I'm Concierge. Chat with me here for anything you need.\n" + + '\n' + + "To manage your team's expenses, create a workspace to keep everything in one place. Here's how:\n" + + '1. From the home screen, click the green + button > New Workspace\n' + + '2. Give your workspace a name (e.g. “Sales team expenses”).\n' + + '\n' + + 'Then, invite your team to your workspace via the Members pane and connect a business bank account to reimburse them. Let me know if you have any questions!', + [onboardingChoices.PERSONAL_SPEND]: + "# Welcome to Expensify, let's start tracking your expenses!\n" + + "Hi there, I'm Concierge. Chat with me here for anything you need.\n" + + '\n' + + "To track your expenses, create a workspace to keep everything in one place. Here's how:\n" + + '1. From the home screen, click the green + button > New Workspace\n' + + '2. Give your workspace a name (e.g. "My expenses”).\n' + + '\n' + + 'Then, add expenses to your workspace:\n' + + '1. Find your workspace using the search field.\n' + + '2. Click the gray + button next to the message field.\n' + + '3. Click Request money, then add your expense type.\n' + + '\n' + + "We'll store all expenses in your new workspace for easy access. Let me know if you have any questions!", + [onboardingChoices.CHAT_SPLIT]: + '# Welcome to Expensify, where splitting the bill is an easy conversation!\n' + + "Hi there, I'm Concierge. Chat with me here for anything you need.\n" + + '\n' + + 'To split an expense:\n' + + '1. From the home screen, click the green + button > Request money.\n' + + '2. Enter an amount or scan a receipt, then choose who you want to split it with.\n' + + '\n' + + "We'll send a request to each person so they can pay you back. Let me know if you have any questions!", + [onboardingChoices.LOOKING_AROUND]: + '# Welcome to Expensify!\n' + + "Hi there, I'm Concierge. Chat with me here for anything you need.\n" + + '\n' + + "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.", + }, + REPORT_FIELD_TITLE_FIELD_ID: 'text_title', MOBILE_PAGINATION_SIZE: 15, diff --git a/src/NAVIGATORS.ts b/src/NAVIGATORS.ts index 3bc9c5e57b9b..f199d2841ec0 100644 --- a/src/NAVIGATORS.ts +++ b/src/NAVIGATORS.ts @@ -7,5 +7,7 @@ export default { BOTTOM_TAB_NAVIGATOR: 'BottomTabNavigator', LEFT_MODAL_NAVIGATOR: 'LeftModalNavigator', RIGHT_MODAL_NAVIGATOR: 'RightModalNavigator', + ONBOARDING_MODAL_NAVIGATOR: 'OnboardingModalNavigator', + WELCOME_VIDEO_MODAL_NAVIGATOR: 'WelcomeVideoModalNavigator', FULL_SCREEN_NAVIGATOR: 'FullScreenNavigator', } as const; diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 0046e076e056..0a21cb17df7e 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -276,6 +276,9 @@ const ONYXKEYS = { // Max height supported for HTML element MAX_CANVAS_HEIGHT: 'maxCanvasHeight', + /** Onboarding Purpose selected by the user during Onboarding flow */ + ONBOARDING_PURPOSE_SELECTED: 'onboardingPurposeSelected', + // Max width supported for HTML element MAX_CANVAS_WIDTH: 'maxCanvasWidth', @@ -374,6 +377,8 @@ const ONYXKEYS = { PROFILE_SETTINGS_FORM_DRAFT: 'profileSettingsFormDraft', DISPLAY_NAME_FORM: 'displayNameForm', DISPLAY_NAME_FORM_DRAFT: 'displayNameFormDraft', + ONBOARDING_PERSONAL_DETAILS_FORM: 'onboardingPersonalDetailsForm', + ONBOARDING_PERSONAL_DETAILS_FORM_DRAFT: 'onboardingPersonalDetailsFormDraft', ROOM_NAME_FORM: 'roomNameForm', ROOM_NAME_FORM_DRAFT: 'roomNameFormDraft', REPORT_DESCRIPTION_FORM: 'reportDescriptionForm', @@ -461,6 +466,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM]: FormTypes.CloseAccountForm; [ONYXKEYS.FORMS.PROFILE_SETTINGS_FORM]: FormTypes.ProfileSettingsForm; [ONYXKEYS.FORMS.DISPLAY_NAME_FORM]: FormTypes.DisplayNameForm; + [ONYXKEYS.FORMS.ONBOARDING_PERSONAL_DETAILS_FORM]: FormTypes.DisplayNameForm; [ONYXKEYS.FORMS.ROOM_NAME_FORM]: FormTypes.RoomNameForm; [ONYXKEYS.FORMS.REPORT_DESCRIPTION_FORM]: FormTypes.ReportDescriptionForm; [ONYXKEYS.FORMS.LEGAL_NAME_FORM]: FormTypes.LegalNameForm; @@ -627,6 +633,7 @@ type OnyxValuesMapping = { [ONYXKEYS.MAX_CANVAS_AREA]: number; [ONYXKEYS.MAX_CANVAS_HEIGHT]: number; [ONYXKEYS.MAX_CANVAS_WIDTH]: number; + [ONYXKEYS.ONBOARDING_PURPOSE_SELECTED]: string; [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: boolean; [ONYXKEYS.LAST_VISITED_PATH]: string | undefined; [ONYXKEYS.RECENTLY_USED_REPORT_FIELDS]: OnyxTypes.RecentlyUsedReportFields; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 4034db2ee0c1..7efc1f676f26 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -682,6 +682,11 @@ const ROUTES = { getRoute: (contentType: string, backTo?: string) => getUrlWithBackToParam(`referral/${contentType}`, backTo), }, PROCESS_MONEY_REQUEST_HOLD: 'hold-request-educational', + ONBOARDING_ROOT: 'onboarding', + ONBOARDING_PERSONAL_DETAILS: 'onboarding/personal-details', + ONBOARDING_PURPOSE: 'onboarding/purpose', + WELCOME_VIDEO_ROOT: 'onboarding/welcome-video', + TRANSACTION_RECEIPT: { route: 'r/:reportID/transaction/:transactionID/receipt', getRoute: (reportID: string, transactionID: string) => `r/${reportID}/transaction/${transactionID}/receipt` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index c1d6a5669fbc..fe1b1eac510f 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -127,6 +127,9 @@ const SCREENS = { REFERRAL: 'Referral', PROCESS_MONEY_REQUEST_HOLD: 'ProcessMoneyRequestHold', }, + ONBOARDING_MODAL: { + ONBOARDING: 'Onboarding', + }, SIGN_IN_WITH_APPLE_DESKTOP: 'AppleSignInDesktop', SIGN_IN_WITH_GOOGLE_DESKTOP: 'GoogleSignInDesktop', DESKTOP_SIGN_IN_REDIRECT: 'DesktopSignInRedirect', @@ -272,12 +275,21 @@ const SCREENS = { EDIT_CURRENCY: 'SplitDetails_Edit_Currency', }, + ONBOARDING: { + PERSONAL_DETAILS: 'Onboarding_Personal_Details', + PURPOSE: 'Onboarding_Purpose', + }, + ONBOARD_ENGAGEMENT: { ROOT: 'Onboard_Engagement_Root', MANAGE_TEAMS_EXPENSES: 'Manage_Teams_Expenses', EXPENSIFY_CLASSIC: 'Expenisfy_Classic', }, + WELCOME_VIDEO: { + ROOT: 'Welcome_Video_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/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index e9c5b80b37ac..3d20f910dca0 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -66,6 +66,9 @@ type FormProviderProps = FormProvider /** Should validate function be called when the value of the input is changed */ shouldValidateOnChange?: boolean; + /** Whether to remove invisible characters from strings before validation and submission */ + shouldTrimValues?: boolean; + /** Styles that will be applied to the submit button only */ submitButtonStyles?: StyleProp; @@ -85,6 +88,7 @@ function FormProvider( enabledWhenOffline = false, draftValues, onSubmit, + shouldTrimValues = true, ...rest }: FormProviderProps, forwardedRef: ForwardedRef, @@ -98,7 +102,7 @@ function FormProvider( const onValidate = useCallback( (values: FormOnyxValues, shouldClearServerError = true) => { - const trimmedStringValues = ValidationUtils.prepareValues(values); + const trimmedStringValues = shouldTrimValues ? ValidationUtils.prepareValues(values) : values; if (shouldClearServerError) { FormActions.clearErrors(formID); @@ -154,7 +158,7 @@ function FormProvider( return touchedInputErrors; }, - [errors, formID, validate], + [errors, formID, validate, shouldTrimValues], ); // When locales change from another session of the same account, @@ -166,7 +170,7 @@ function FormProvider( } // Prepare validation values - const trimmedStringValues = ValidationUtils.prepareValues(inputValues); + const trimmedStringValues = shouldTrimValues ? ValidationUtils.prepareValues(inputValues) : inputValues; // Validate in order to make sure the correct error translations are displayed, // making sure to not clear server errors if they exist @@ -191,7 +195,7 @@ function FormProvider( } // Prepare values before submitting - const trimmedStringValues = ValidationUtils.prepareValues(inputValues); + const trimmedStringValues = shouldTrimValues ? ValidationUtils.prepareValues(inputValues) : inputValues; // Touches all form inputs, so we can validate the entire form Object.keys(inputRefs.current).forEach((inputID) => (touchedInputs.current[inputID] = true)); @@ -207,7 +211,7 @@ function FormProvider( } onSubmit(trimmedStringValues); - }, [enabledWhenOffline, formState?.isLoading, inputValues, network?.isOffline, onSubmit, onValidate]); + }, [enabledWhenOffline, formState?.isLoading, inputValues, network?.isOffline, onSubmit, onValidate, shouldTrimValues]); const resetForm = useCallback( (optionalValue: FormOnyxValues) => { diff --git a/src/components/HeaderWithBackButton/index.tsx b/src/components/HeaderWithBackButton/index.tsx index f3293596aa46..0cbed7a2b481 100755 --- a/src/components/HeaderWithBackButton/index.tsx +++ b/src/components/HeaderWithBackButton/index.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useMemo} from 'react'; import {Keyboard, StyleSheet, View} from 'react-native'; import Avatar from '@components/Avatar'; import AvatarWithDisplayName from '@components/AvatarWithDisplayName'; @@ -58,6 +58,7 @@ function HeaderWithBackButton({ shouldOverlayDots = false, shouldOverlay = false, shouldNavigateToTopMostReport = false, + progressBarPercentage, style, }: HeaderWithBackButtonProps) { const theme = useTheme(); @@ -70,6 +71,60 @@ function HeaderWithBackButton({ // If the icon is present, the header bar should be taller and use different font. const isCentralPaneSettings = !!icon; + const middleContent = useMemo(() => { + if (progressBarPercentage) { + return ( + <> + {/* Reserves as much space for the middleContent as possible */} + + {/* Uses absolute positioning so that it's always centered instead of being affected by the + presence or absence of back/close buttons to the left/right of it */} + + + + + + + ); + } + + if (shouldShowReportAvatarWithDisplay) { + return ( + + ); + } + + return ( +
+ ); + }, [ + StyleUtils, + isCentralPaneSettings, + policy, + progressBarPercentage, + report, + shouldEnableDetailPageNavigation, + shouldShowReportAvatarWithDisplay, + stepCounter, + styles.flexGrow1, + styles.headerProgressBar, + styles.headerProgressBarContainer, + styles.headerProgressBarFill, + styles.textHeadlineH2, + subtitle, + title, + titleColor, + translate, + ]); + return ( )} - {shouldShowReportAvatarWithDisplay ? ( - - ) : ( -
- )} + {middleContent} {children} {shouldShowDownloadButton && ( diff --git a/src/components/HeaderWithBackButton/types.ts b/src/components/HeaderWithBackButton/types.ts index 3b7da9f77287..920abf3d08f0 100644 --- a/src/components/HeaderWithBackButton/types.ts +++ b/src/components/HeaderWithBackButton/types.ts @@ -121,6 +121,9 @@ type HeaderWithBackButtonProps = Partial & { /** Whether we should overlay the 3 dots menu */ shouldOverlayDots?: boolean; + /** 0 - 100 number indicating current progress of the progress bar */ + progressBarPercentage?: number; + /** Policy avatar to display in the header */ policyAvatar?: Icon; diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts index 28d1d53ed60c..58ba14916a3b 100644 --- a/src/components/Icon/Illustrations.ts +++ b/src/components/Icon/Illustrations.ts @@ -29,17 +29,20 @@ import TadaYellow from '@assets/images/product-illustrations/tada--yellow.svg'; import TeleScope from '@assets/images/product-illustrations/telescope.svg'; import ThreeLeggedLaptopWoman from '@assets/images/product-illustrations/three_legged_laptop_woman.svg'; import ToddBehindCloud from '@assets/images/product-illustrations/todd-behind-cloud.svg'; +import Abacus from '@assets/images/simple-illustrations/simple-illustration__abacus.svg'; import Accounting from '@assets/images/simple-illustrations/simple-illustration__accounting.svg'; import Approval from '@assets/images/simple-illustrations/simple-illustration__approval.svg'; import BankArrow from '@assets/images/simple-illustrations/simple-illustration__bank-arrow.svg'; import BigRocket from '@assets/images/simple-illustrations/simple-illustration__bigrocket.svg'; import PinkBill from '@assets/images/simple-illustrations/simple-illustration__bill.svg'; +import Binoculars from '@assets/images/simple-illustrations/simple-illustration__binoculars.svg'; import CarIce from '@assets/images/simple-illustrations/simple-illustration__car-ice.svg'; import Car from '@assets/images/simple-illustrations/simple-illustration__car.svg'; import ChatBubbles from '@assets/images/simple-illustrations/simple-illustration__chatbubbles.svg'; import CoffeeMug from '@assets/images/simple-illustrations/simple-illustration__coffeemug.svg'; import Coins from '@assets/images/simple-illustrations/simple-illustration__coins.svg'; import CommentBubbles from '@assets/images/simple-illustrations/simple-illustration__commentbubbles.svg'; +import CompanyCard from '@assets/images/simple-illustrations/simple-illustration__company-card.svg'; import ConciergeBubble from '@assets/images/simple-illustrations/simple-illustration__concierge-bubble.svg'; import ConciergeNew from '@assets/images/simple-illustrations/simple-illustration__concierge.svg'; import CreditCardsNew from '@assets/images/simple-illustrations/simple-illustration__credit-cards.svg'; @@ -63,13 +66,16 @@ import MoneyWings from '@assets/images/simple-illustrations/simple-illustration_ import OpenSafe from '@assets/images/simple-illustrations/simple-illustration__opensafe.svg'; import PalmTree from '@assets/images/simple-illustrations/simple-illustration__palmtree.svg'; import Pencil from '@assets/images/simple-illustrations/simple-illustration__pencil.svg'; +import PiggyBank from '@assets/images/simple-illustrations/simple-illustration__piggybank.svg'; import Profile from '@assets/images/simple-illustrations/simple-illustration__profile.svg'; import QRCode from '@assets/images/simple-illustrations/simple-illustration__qr-code.svg'; import ReceiptEnvelope from '@assets/images/simple-illustrations/simple-illustration__receipt-envelope.svg'; import ReceiptWrangler from '@assets/images/simple-illustrations/simple-illustration__receipt-wrangler.svg'; +import ReceiptUpload from '@assets/images/simple-illustrations/simple-illustration__receiptupload.svg'; import SanFrancisco from '@assets/images/simple-illustrations/simple-illustration__sanfrancisco.svg'; import ShieldYellow from '@assets/images/simple-illustrations/simple-illustration__shield.svg'; import SmallRocket from '@assets/images/simple-illustrations/simple-illustration__smallrocket.svg'; +import SplitBill from '@assets/images/simple-illustrations/simple-illustration__splitbill.svg'; import Tag from '@assets/images/simple-illustrations/simple-illustration__tag.svg'; import ThumbsUpStars from '@assets/images/simple-illustrations/simple-illustration__thumbsupstars.svg'; import TrackShoe from '@assets/images/simple-illustrations/simple-illustration__track-shoe.svg'; @@ -152,6 +158,12 @@ export { Workflows, ThreeLeggedLaptopWoman, House, + Abacus, + Binoculars, + CompanyCard, + ReceiptUpload, + SplitBill, + PiggyBank, Accounting, Car, Coins, diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 4d6f79bd0196..12ddf04658f4 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -438,8 +438,8 @@ function MenuItem( combinedStyle, !interactive && styles.cursorDefault, StyleUtils.getButtonBackgroundColorStyle(getButtonState(focused || isHovered, pressed, success, disabled, interactive), true), - !focused && (isHovered || pressed) && hoverAndPressStyle, ...(Array.isArray(wrapperStyle) ? wrapperStyle : [wrapperStyle]), + !focused && (isHovered || pressed) && hoverAndPressStyle, shouldGreyOutWhenDisabled && disabled && styles.buttonOpacityDisabled, ] as StyleProp } diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx index 4604e831a2ae..1261b418d27e 100644 --- a/src/components/Modal/BaseModal.tsx +++ b/src/components/Modal/BaseModal.tsx @@ -215,7 +215,7 @@ function BaseModal( customBackdrop={shouldUseCustomBackdrop ? : undefined} > {children} diff --git a/src/components/OnboardingWelcomeVideo.tsx b/src/components/OnboardingWelcomeVideo.tsx new file mode 100644 index 000000000000..f38bd4edcc10 --- /dev/null +++ b/src/components/OnboardingWelcomeVideo.tsx @@ -0,0 +1,161 @@ +import type {VideoReadyForDisplayEvent} from 'expo-av'; +import React, {useCallback, useEffect, useState} from 'react'; +import {View} from 'react-native'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import useOnboardingLayout from '@hooks/useOnboardingLayout'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import Navigation from '@libs/Navigation/Navigation'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import Button from './Button'; +import Lottie from './Lottie'; +import LottieAnimations from './LottieAnimations'; +import Modal from './Modal'; +import SafeAreaConsumer from './SafeAreaConsumer'; +import Text from './Text'; +import VideoPlayer from './VideoPlayer'; + +// Aspect ratio and height of the video. +// Useful before video loads to reserve space. +const VIDEO_ASPECT_RATIO = 1280 / 960; + +const MODAL_PADDING = variables.spacing2; + +type VideoLoadedEventType = { + srcElement: { + videoWidth: number; + videoHeight: number; + }; +}; + +type VideoStatus = 'video' | 'animation'; + +function OnboardingWelcomeVideo() { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const [isModalVisible, setIsModalVisible] = useState(true); + const {shouldUseNarrowLayout} = useOnboardingLayout(); + const [welcomeVideoStatus, setWelcomeVideoStatus] = useState('video'); + const [isWelcomeVideoStatusLocked, setIsWelcomeVideoStatusLocked] = useState(false); + const [videoAspectRatio, setVideoAspectRatio] = useState(VIDEO_ASPECT_RATIO); + const {isSmallScreenWidth} = useWindowDimensions(); + const {isOffline} = useNetwork(); + + useEffect(() => { + if (isWelcomeVideoStatusLocked) { + return; + } + + if (isOffline) { + setWelcomeVideoStatus('animation'); + } else if (!isOffline) { + setWelcomeVideoStatus('video'); + setIsWelcomeVideoStatusLocked(true); + } + }, [isOffline, isWelcomeVideoStatusLocked]); + + const closeModal = useCallback(() => { + setIsModalVisible(false); + Navigation.goBack(); + }, []); + + const setAspectRatio = (event: VideoReadyForDisplayEvent | VideoLoadedEventType | undefined) => { + if (!event) { + return; + } + + if ('naturalSize' in event) { + setVideoAspectRatio(event.naturalSize.width / event.naturalSize.height); + } else { + setVideoAspectRatio(event.srcElement.videoWidth / event.srcElement.videoHeight); + } + }; + + const getWelcomeVideo = () => { + const aspectRatio = videoAspectRatio || VIDEO_ASPECT_RATIO; + + return ( + + {welcomeVideoStatus === 'video' ? ( + + ) : ( + + + + )} + + ); + }; + + return ( + + {({safeAreaPaddingBottomStyle}) => ( + + + {getWelcomeVideo()} + + + {translate('onboarding.welcomeVideo.title')} + {translate('onboarding.welcomeVideo.description')} + +