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')}
+
+
+
+
+
+ )}
+
+ );
+}
+
+OnboardingWelcomeVideo.displayName = 'OnboardingWelcomeVideo';
+
+export default OnboardingWelcomeVideo;
diff --git a/src/components/TestToolMenu.tsx b/src/components/TestToolMenu.tsx
index 647f25c450e5..527b92d4d7dc 100644
--- a/src/components/TestToolMenu.tsx
+++ b/src/components/TestToolMenu.tsx
@@ -4,11 +4,13 @@ import {withOnyx} from 'react-native-onyx';
import useThemeStyles from '@hooks/useThemeStyles';
import * as ApiUtils from '@libs/ApiUtils';
import compose from '@libs/compose';
+import Navigation from '@libs/Navigation/Navigation';
import * as Network from '@userActions/Network';
import * as Session from '@userActions/Session';
import * as User from '@userActions/User';
import CONFIG from '@src/CONFIG';
import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
import type {Network as NetworkOnyx, User as UserOnyx} from '@src/types/onyx';
import Button from './Button';
import {withNetwork} from './OnyxProvider';
@@ -87,6 +89,18 @@ function TestToolMenu({user = USER_DEFAULT, network}: TestToolMenuProps) {
onPress={() => Session.invalidateCredentials()}
/>
+
+ {/* Navigate to the new Onboarding flow (Stage 1). This button is temporary and should be removed after passing QA tests. */}
+
+ {
+ Navigation.dismissModal();
+ Navigation.navigate(ROUTES.ONBOARDING_PERSONAL_DETAILS);
+ }}
+ />
+
>
);
}
diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx
index 5406740b6141..9380ce43c46a 100644
--- a/src/components/VideoPlayer/BaseVideoPlayer.tsx
+++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx
@@ -30,12 +30,13 @@ function BaseVideoPlayer({
videoPlayerStyle,
videoStyle,
videoControlsStyle,
- videoDuration,
+ videoDuration = 0,
+ controlsStatus = CONST.VIDEO_PLAYER.CONTROLS_STATUS.SHOW,
shouldUseSharedVideoElement = false,
shouldUseSmallVideoControls = false,
- shouldShowVideoControls = true,
onPlaybackStatusUpdate,
onFullscreenUpdate,
+ shouldPlay,
// TODO: investigate what is the root cause of the bug with unexpected video switching
// isVideoHovered caused a bug with unexpected video switching. We are investigating the root cause of the issue,
// but current workaround is just not to use it here for now. This causes not displaying the video controls when
@@ -221,6 +222,13 @@ function BaseVideoPlayer({
};
}, [bindFunctions, currentVideoPlayerRef, currentlyPlayingURL, isFullScreenRef, originalParent, sharedElement, shouldUseSharedVideoElement, url]);
+ useEffect(() => {
+ if (!shouldPlay) {
+ return;
+ }
+ updateCurrentlyPlayingURL(url);
+ }, [shouldPlay, updateCurrentlyPlayingURL, url]);
+
return (
<>
{/* We need to wrap the video component in a component that will catch unhandled pointer events. Otherwise, these
@@ -278,7 +286,7 @@ function BaseVideoPlayer({
// reset the video player after connection is back
uri: !isLoading || (isLoading && !isOffline) ? sourceURL : '',
}}
- shouldPlay={false}
+ shouldPlay={shouldPlay}
useNativeControls={false}
resizeMode={resizeMode as ResizeMode}
isLooping={isLooping}
@@ -297,7 +305,7 @@ function BaseVideoPlayer({
{(isLoading || isBuffering) && }
- {shouldShowVideoControls && !isLoading && (isPopoverVisible || isHovered || canUseTouchScreen) && (
+ {controlsStatus !== CONST.VIDEO_PLAYER.CONTROLS_STATUS.HIDE && !isLoading && (isPopoverVisible || isHovered || canUseTouchScreen) && (
)}
diff --git a/src/components/VideoPlayer/VideoPlayerControls/ProgressBar/index.tsx b/src/components/VideoPlayer/VideoPlayerControls/ProgressBar/index.tsx
index f9dd09db59f4..c9cf2c25d7ad 100644
--- a/src/components/VideoPlayer/VideoPlayerControls/ProgressBar/index.tsx
+++ b/src/components/VideoPlayer/VideoPlayerControls/ProgressBar/index.tsx
@@ -72,7 +72,7 @@ function ProgressBar({duration, position, seekPosition}: ProgressBarProps) {
return (
-
+
void | Promise;
+
+ controlsStatus: ValueOf;
};
-function VideoPlayerControls({duration, position, url, videoPlayerRef, isPlaying, small = false, style, showPopoverMenu, togglePlayCurrentVideo}: VideoPlayerControlsProps) {
+function VideoPlayerControls({
+ duration,
+ position,
+ url,
+ videoPlayerRef,
+ isPlaying,
+ small = false,
+ style,
+ showPopoverMenu,
+ togglePlayCurrentVideo,
+ controlsStatus = CONST.VIDEO_PLAYER.CONTROLS_STATUS.SHOW,
+}: VideoPlayerControlsProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const {updateCurrentlyPlayingURL} = usePlaybackContext();
@@ -74,49 +88,59 @@ function VideoPlayerControls({duration, position, url, videoPlayerRef, isPlaying
return (
-
-
-
- {shouldShowTime && (
-
- {convertMillisecondsToTime(position)}
- /
- {durationFormatted}
-
- )}
+ {controlsStatus === CONST.VIDEO_PLAYER.CONTROLS_STATUS.SHOW && (
+
+
+
+ {shouldShowTime && (
+
+ {convertMillisecondsToTime(position)}
+ /
+ {durationFormatted}
+
+ )}
+
+
+
+
+
+
-
-
-
-
+
+
-
-
-
+ {controlsStatus === CONST.VIDEO_PLAYER.CONTROLS_STATUS.VOLUME_ONLY && }
);
diff --git a/src/components/VideoPlayer/index.native.tsx b/src/components/VideoPlayer/index.native.tsx
index d7d99ed2d726..6375e8688e48 100644
--- a/src/components/VideoPlayer/index.native.tsx
+++ b/src/components/VideoPlayer/index.native.tsx
@@ -3,14 +3,14 @@ import CONST from '@src/CONST';
import BaseVideoPlayer from './BaseVideoPlayer';
import type {VideoPlayerProps} from './types';
-function VideoPlayer({videoControlsStyle, ...props}: VideoPlayerProps) {
+function VideoPlayer({videoControlsStyle, shouldUseControlsBottomMargin = true, ...props}: VideoPlayerProps) {
return (
);
}
diff --git a/src/components/VideoPlayer/types.ts b/src/components/VideoPlayer/types.ts
index 8b9205ce8c9f..d27b1dcdbcdd 100644
--- a/src/components/VideoPlayer/types.ts
+++ b/src/components/VideoPlayer/types.ts
@@ -2,6 +2,8 @@ import type {Video} from 'expo-av';
import type {AVPlaybackStatus} from 'expo-av/build/AV';
import type {VideoFullscreenUpdateEvent, VideoReadyForDisplayEvent} from 'expo-av/build/Video.types';
import type {StyleProp, ViewStyle} from 'react-native';
+import type {ValueOf} from 'type-fest';
+import type CONST from '@src/CONST';
type VideoWithOnFullScreenUpdate = Video & {_onFullscreenUpdate: (event: VideoFullscreenUpdateEvent) => void};
@@ -11,19 +13,22 @@ type VideoPlayerProps = {
resizeMode?: string;
isLooping?: boolean;
// style for the whole video player component
- style: StyleProp;
+ style?: StyleProp;
// style for the video player inside the component
videoPlayerStyle?: StyleProp;
// style for the video element inside the video player
videoStyle?: StyleProp;
videoControlsStyle?: StyleProp;
- videoDuration: number;
+ videoDuration?: number;
shouldUseSharedVideoElement?: boolean;
shouldUseSmallVideoControls?: boolean;
shouldShowVideoControls?: boolean;
isVideoHovered?: boolean;
onFullscreenUpdate?: (event: VideoFullscreenUpdateEvent) => void;
onPlaybackStatusUpdate?: (status: AVPlaybackStatus) => void;
+ shouldUseControlsBottomMargin?: boolean;
+ controlsStatus?: ValueOf;
+ shouldPlay?: boolean;
};
export type {VideoPlayerProps, VideoWithOnFullScreenUpdate};
diff --git a/src/hooks/useOnboardingLayout.ts b/src/hooks/useOnboardingLayout.ts
new file mode 100644
index 000000000000..512482340bb2
--- /dev/null
+++ b/src/hooks/useOnboardingLayout.ts
@@ -0,0 +1,19 @@
+// eslint-disable-next-line no-restricted-imports
+import {useWindowDimensions} from 'react-native';
+import variables from '@styles/variables';
+
+type OnboardingLayout = {
+ shouldUseNarrowLayout: boolean;
+};
+
+/**
+ * The main difference between useOnboardingLayout and useWindowDimension is that
+ * useWindowDimension hardcodes isSmallScreenWidth, isMediumScreenWidth and
+ * isLargeScreenWidth on native platforms, while this hook below always takes
+ * screen width into consideration, no matter the platform.
+ */
+export default function useOnboardingLayout(): OnboardingLayout {
+ const {width: windowWidth} = useWindowDimensions();
+
+ return {shouldUseNarrowLayout: windowWidth > variables.mobileResponsiveWidthBreakpoint};
+}
diff --git a/src/languages/en.ts b/src/languages/en.ts
index b51cdf2309e1..55a4c586716a 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -1314,6 +1314,29 @@ export default {
loginForm: 'Login form',
notYou: ({user}: NotYouParams) => `Not ${user}?`,
},
+ onboarding: {
+ welcome: 'Welcome!',
+ welcomeVideo: {
+ title: 'Welcome to Expensify',
+ description: 'Getting paid is as easy as sending a message.',
+ button: "Let's go",
+ },
+ whatsYourName: "What's your name?",
+ purpose: {
+ title: 'What do you want to do today?',
+ error: 'Please make a selection before continuing',
+ [CONST.ONBOARDING_CHOICES.TRACK]: 'Track business spend for taxes',
+ [CONST.ONBOARDING_CHOICES.EMPLOYER]: 'Get paid back by my employer',
+ [CONST.ONBOARDING_CHOICES.MANAGE_TEAM]: "Manage my team's expenses",
+ [CONST.ONBOARDING_CHOICES.PERSONAL_SPEND]: 'Track and budget personal spend',
+ [CONST.ONBOARDING_CHOICES.CHAT_SPLIT]: 'Chat and split bills with friends',
+ [CONST.ONBOARDING_CHOICES.LOOKING_AROUND]: "I'm just looking around",
+ },
+ error: {
+ requiredFirstName: 'Please input your first name to continue',
+ requiredLastName: 'Please input your last name to continue',
+ },
+ },
personalDetails: {
error: {
containsReservedWord: 'Name cannot contain the words Expensify or Concierge',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 89c3f8c2c686..5956f1457005 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -1316,6 +1316,29 @@ export default {
loginForm: 'Formulario de inicio de sesión',
notYou: ({user}: NotYouParams) => `¿No eres ${user}?`,
},
+ onboarding: {
+ welcome: '¡Bienvenido!',
+ welcomeVideo: {
+ title: 'Bienvenido a Expensify',
+ description: 'Cobrar es tan fácil como enviar un mensaje.',
+ button: 'Vámonos',
+ },
+ whatsYourName: '¿Cómo te llamas?',
+ purpose: {
+ title: '¿Qué quieres hacer hoy?',
+ error: 'Por favor, haga una selección antes de continuar.',
+ [CONST.ONBOARDING_CHOICES.TRACK]: 'Seguimiento fiscal de los gastos de las empresas',
+ [CONST.ONBOARDING_CHOICES.EMPLOYER]: 'Cobrar de mi empresa',
+ [CONST.ONBOARDING_CHOICES.MANAGE_TEAM]: 'Gestionar los gastos de mi equipo',
+ [CONST.ONBOARDING_CHOICES.PERSONAL_SPEND]: 'Controlar y presupuestar los gastos personales',
+ [CONST.ONBOARDING_CHOICES.CHAT_SPLIT]: 'Chatea y divide cuentas con tus amigos',
+ [CONST.ONBOARDING_CHOICES.LOOKING_AROUND]: 'Sólo estoy mirando',
+ },
+ error: {
+ requiredFirstName: 'Introduce tu nombre para continuar',
+ requiredLastName: 'Introduce tu apellido para continuar',
+ },
+ },
personalDetails: {
error: {
containsReservedWord: 'El nombre no puede contener las palabras Expensify o Concierge',
diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
index 463dcfcd9e99..295daa1938e7 100644
--- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx
+++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
@@ -1,7 +1,8 @@
-import React, {memo, useEffect, useRef} from 'react';
+import React, {memo, useEffect, useMemo, useRef} from 'react';
import {View} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import Onyx, {withOnyx} from 'react-native-onyx';
+import useOnboardingLayout from '@hooks/useOnboardingLayout';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
@@ -41,7 +42,9 @@ import BottomTabNavigator from './Navigators/BottomTabNavigator';
import CentralPaneNavigator from './Navigators/CentralPaneNavigator';
import FullScreenNavigator from './Navigators/FullScreenNavigator';
import LeftModalNavigator from './Navigators/LeftModalNavigator';
+import OnboardingModalNavigator from './Navigators/OnboardingModalNavigator';
import RightModalNavigator from './Navigators/RightModalNavigator';
+import WelcomeVideoModalNavigator from './Navigators/WelcomeVideoModalNavigator';
type AuthScreensProps = {
/** Session of currently logged in user */
@@ -154,7 +157,9 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const {isSmallScreenWidth} = useWindowDimensions();
+ const {shouldUseNarrowLayout} = useOnboardingLayout();
const screenOptions = getRootNavigatorScreenOptions(isSmallScreenWidth, styles, StyleUtils);
+ const onboardingScreenOptions = useMemo(() => screenOptions.onboardingModalNavigator(shouldUseNarrowLayout), [screenOptions, shouldUseNarrowLayout]);
const isInitialRender = useRef(true);
if (isInitialRender.current) {
@@ -355,6 +360,16 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie
options={screenOptions.fullScreen}
component={DesktopSignInRedirectPage}
/>
+
+
();
+
+function OnboardingModalNavigator() {
+ const styles = useThemeStyles();
+ const {shouldUseNarrowLayout} = useOnboardingLayout();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+OnboardingModalNavigator.displayName = 'OnboardingModalNavigator';
+
+export default OnboardingModalNavigator;
diff --git a/src/libs/Navigation/AppNavigator/Navigators/Overlay.tsx b/src/libs/Navigation/AppNavigator/Navigators/Overlay/BaseOverlay.tsx
similarity index 79%
rename from src/libs/Navigation/AppNavigator/Navigators/Overlay.tsx
rename to src/libs/Navigation/AppNavigator/Navigators/Overlay/BaseOverlay.tsx
index 5462b6c0ce4e..cc687cd77d94 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/Overlay.tsx
+++ b/src/libs/Navigation/AppNavigator/Navigators/Overlay/BaseOverlay.tsx
@@ -6,21 +6,24 @@ import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
-type OverlayProps = {
+type BaseOverlayProps = {
+ /* Whether to use native styles tailored for native devices */
+ shouldUseNativeStyles: boolean;
+
/* Callback to close the modal */
- onPress: () => void;
+ onPress?: () => void;
/* Returns whether a modal is displayed on the left side of the screen. By default, the modal is displayed on the right */
isModalOnTheLeft?: boolean;
};
-function Overlay({onPress, isModalOnTheLeft = false}: OverlayProps) {
+function BaseOverlay({shouldUseNativeStyles, onPress, isModalOnTheLeft = false}: BaseOverlayProps) {
const styles = useThemeStyles();
const {current} = useCardAnimation();
const {translate} = useLocalize();
return (
-
+
{/* In the latest Electron version buttons can't be both clickable and draggable.
That's why we added this workaround. Because of two Pressable components on the desktop app
@@ -48,6 +51,7 @@ function Overlay({onPress, isModalOnTheLeft = false}: OverlayProps) {
);
}
-Overlay.displayName = 'Overlay';
+BaseOverlay.displayName = 'BaseOverlay';
-export default Overlay;
+export type {BaseOverlayProps};
+export default BaseOverlay;
diff --git a/src/libs/Navigation/AppNavigator/Navigators/Overlay/index.native.tsx b/src/libs/Navigation/AppNavigator/Navigators/Overlay/index.native.tsx
new file mode 100644
index 000000000000..0dd9e203c46b
--- /dev/null
+++ b/src/libs/Navigation/AppNavigator/Navigators/Overlay/index.native.tsx
@@ -0,0 +1,16 @@
+import React from 'react';
+import type {BaseOverlayProps} from './BaseOverlay';
+import BaseOverlay from './BaseOverlay';
+
+function Overlay({...rest}: Omit) {
+ return (
+
+ );
+}
+
+Overlay.displayName = 'Overlay';
+export default Overlay;
diff --git a/src/libs/Navigation/AppNavigator/Navigators/Overlay/index.tsx b/src/libs/Navigation/AppNavigator/Navigators/Overlay/index.tsx
new file mode 100644
index 000000000000..c3f9710b90b4
--- /dev/null
+++ b/src/libs/Navigation/AppNavigator/Navigators/Overlay/index.tsx
@@ -0,0 +1,16 @@
+import React from 'react';
+import type {BaseOverlayProps} from './BaseOverlay';
+import BaseOverlay from './BaseOverlay';
+
+function Overlay({...rest}: Omit) {
+ return (
+
+ );
+}
+
+Overlay.displayName = 'Overlay';
+export default Overlay;
diff --git a/src/libs/Navigation/AppNavigator/Navigators/WelcomeVideoModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/WelcomeVideoModalNavigator.tsx
new file mode 100644
index 000000000000..c788af3f8d2b
--- /dev/null
+++ b/src/libs/Navigation/AppNavigator/Navigators/WelcomeVideoModalNavigator.tsx
@@ -0,0 +1,28 @@
+import {createStackNavigator} from '@react-navigation/stack';
+import React from 'react';
+import {View} from 'react-native';
+import NoDropZone from '@components/DragAndDrop/NoDropZone';
+import OnboardingWelcomeVideo from '@components/OnboardingWelcomeVideo';
+import type {WelcomeVideoModalNavigatorParamList} from '@libs/Navigation/types';
+import SCREENS from '@src/SCREENS';
+
+const Stack = createStackNavigator();
+
+function WelcomeVideoModalNavigator() {
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+WelcomeVideoModalNavigator.displayName = 'WelcomeVideoModalNavigator';
+
+export default WelcomeVideoModalNavigator;
diff --git a/src/libs/Navigation/AppNavigator/OnboardingModalNavigatorScreenOptions.ts b/src/libs/Navigation/AppNavigator/OnboardingModalNavigatorScreenOptions.ts
new file mode 100644
index 000000000000..d4607a809f17
--- /dev/null
+++ b/src/libs/Navigation/AppNavigator/OnboardingModalNavigatorScreenOptions.ts
@@ -0,0 +1,16 @@
+import type {StackNavigationOptions} from '@react-navigation/stack';
+import {CardStyleInterpolators} from '@react-navigation/stack';
+
+/**
+ * Modal stack navigator screen options generator function
+ * @returns The screen options object
+ */
+const OnboardingModalNavigatorScreenOptions = (): StackNavigationOptions => ({
+ headerShown: false,
+ animationEnabled: true,
+ gestureDirection: 'horizontal',
+ cardStyleInterpolator: CardStyleInterpolators.forHorizontalIOS,
+ presentation: 'transparentModal',
+});
+
+export default OnboardingModalNavigatorScreenOptions;
diff --git a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx
index 6d20361b75f5..391468578fab 100644
--- a/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx
+++ b/src/libs/Navigation/AppNavigator/createCustomBottomTabNavigator/BottomTabBar.tsx
@@ -43,11 +43,22 @@ function BottomTabBar({isLoadingApp = false}: PurposeForUsingExpensifyModalProps
const navigationState = navigation.getState() as State | undefined;
const routes = navigationState?.routes;
const currentRoute = routes?.[navigationState?.index ?? 0];
+
if (Boolean(currentRoute && currentRoute.name !== NAVIGATORS.BOTTOM_TAB_NAVIGATOR && currentRoute.name !== NAVIGATORS.CENTRAL_PANE_NAVIGATOR) || Session.isAnonymousUser()) {
return;
}
- Welcome.show(routes, () => Navigation.navigate(ROUTES.ONBOARD));
+ // Welcome.isOnboardingFlowCompleted({onNotCompleted: () => Navigation.navigate(ROUTES.ONBOARDING_PERSONAL_DETAILS)});
+ Welcome.isOnboardingFlowCompleted({
+ onNotCompleted: () =>
+ Navigation.navigate(
+ // Uncomment once Stage 1 Onboarding Flow is ready
+ //
+ // ROUTES.ONBOARDING_PERSONAL_DETAILS
+ //
+ ROUTES.ONBOARD,
+ ),
+ });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLoadingApp]);
diff --git a/src/libs/Navigation/AppNavigator/createModalCardStyleInterpolator.ts b/src/libs/Navigation/AppNavigator/createModalCardStyleInterpolator.ts
index 7a88976b3e03..21911ebb56e2 100644
--- a/src/libs/Navigation/AppNavigator/createModalCardStyleInterpolator.ts
+++ b/src/libs/Navigation/AppNavigator/createModalCardStyleInterpolator.ts
@@ -6,6 +6,7 @@ import variables from '@styles/variables';
type ModalCardStyleInterpolator = (
isSmallScreenWidth: boolean,
isFullScreenModal: boolean,
+ shouldUseNarrowLayout: boolean,
stackCardInterpolationProps: StackCardInterpolationProps,
outputRangeMultiplier?: number,
) => StackCardInterpolatedStyle;
@@ -13,7 +14,15 @@ type CreateModalCardStyleInterpolator = (StyleUtils: StyleUtilsType) => ModalCar
const createModalCardStyleInterpolator: CreateModalCardStyleInterpolator =
(StyleUtils) =>
- (isSmallScreenWidth, isFullScreenModal, {current: {progress}, inverted, layouts: {screen}}, outputRangeMultiplier = 1) => {
+ (isSmallScreenWidth, isFullScreenModal, shouldUseNarrowLayout, {current: {progress}, inverted, layouts: {screen}}, outputRangeMultiplier = 1) => {
+ if (shouldUseNarrowLayout) {
+ return {
+ cardStyle: {
+ opacity: progress,
+ },
+ };
+ }
+
const translateX = Animated.multiply(
progress.interpolate({
inputRange: [0, 1],
diff --git a/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.ts b/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.ts
index 1dd9322e0b47..0c3b5fdadadd 100644
--- a/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.ts
+++ b/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.ts
@@ -1,11 +1,22 @@
import type {StackCardInterpolationProps, StackNavigationOptions} from '@react-navigation/stack';
+import type {ViewStyle} from 'react-native';
import type {ThemeStyles} from '@styles/index';
import type {StyleUtilsType} from '@styles/utils';
import variables from '@styles/variables';
import CONFIG from '@src/CONFIG';
import createModalCardStyleInterpolator from './createModalCardStyleInterpolator';
-type ScreenOptions = Record;
+type GetOnboardingModalNavigatorOptions = (shouldUseNarrowLayout: boolean) => StackNavigationOptions;
+
+type ScreenOptions = {
+ rightModalNavigator: StackNavigationOptions;
+ onboardingModalNavigator: GetOnboardingModalNavigatorOptions;
+ leftModalNavigator: StackNavigationOptions;
+ homeScreen: StackNavigationOptions;
+ fullScreen: StackNavigationOptions;
+ centralPaneNavigator: StackNavigationOptions;
+ bottomTab: StackNavigationOptions;
+};
const commonScreenOptions: StackNavigationOptions = {
headerShown: false,
@@ -23,7 +34,7 @@ const getRootNavigatorScreenOptions: GetRootNavigatorScreenOptions = (isSmallScr
return {
rightModalNavigator: {
...commonScreenOptions,
- cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator(isSmallScreenWidth, false, props),
+ cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator(isSmallScreenWidth, false, false, props),
presentation: 'transparentModal',
// We want pop in RHP since there are some flows that would work weird otherwise
@@ -37,9 +48,25 @@ const getRootNavigatorScreenOptions: GetRootNavigatorScreenOptions = (isSmallScr
right: 0,
},
},
+ onboardingModalNavigator: (shouldUseNarrowLayout: boolean) => ({
+ cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator(isSmallScreenWidth, false, shouldUseNarrowLayout, props),
+ headerShown: false,
+ animationEnabled: true,
+ cardOverlayEnabled: false,
+ presentation: 'transparentModal',
+ cardStyle: {
+ ...StyleUtils.getNavigationModalCardStyle(),
+ backgroundColor: 'transparent',
+ width: '100%',
+ top: 0,
+ left: 0,
+ // We need to guarantee that it covers BottomTabBar on web, but fixed position is not supported in react native.
+ position: 'fixed' as ViewStyle['position'],
+ },
+ }),
leftModalNavigator: {
...commonScreenOptions,
- cardStyleInterpolator: (props) => modalCardStyleInterpolator(isSmallScreenWidth, false, props),
+ cardStyleInterpolator: (props) => modalCardStyleInterpolator(isSmallScreenWidth, false, false, props),
presentation: 'transparentModal',
gestureDirection: 'horizontal-inverted',
@@ -58,7 +85,7 @@ const getRootNavigatorScreenOptions: GetRootNavigatorScreenOptions = (isSmallScr
homeScreen: {
title: CONFIG.SITE_TITLE,
...commonScreenOptions,
- cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator(isSmallScreenWidth, false, props),
+ cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator(isSmallScreenWidth, false, false, props),
cardStyle: {
...StyleUtils.getNavigationModalCardStyle(),
@@ -72,7 +99,7 @@ const getRootNavigatorScreenOptions: GetRootNavigatorScreenOptions = (isSmallScr
fullScreen: {
...commonScreenOptions,
- cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator(isSmallScreenWidth, true, props),
+ cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator(isSmallScreenWidth, true, false, props),
cardStyle: {
...StyleUtils.getNavigationModalCardStyle(),
@@ -85,7 +112,7 @@ const getRootNavigatorScreenOptions: GetRootNavigatorScreenOptions = (isSmallScr
title: CONFIG.SITE_TITLE,
...commonScreenOptions,
animationEnabled: isSmallScreenWidth,
- cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator(isSmallScreenWidth, true, props),
+ cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator(isSmallScreenWidth, true, false, props),
cardStyle: {
...StyleUtils.getNavigationModalCardStyle(),
@@ -95,7 +122,7 @@ const getRootNavigatorScreenOptions: GetRootNavigatorScreenOptions = (isSmallScr
bottomTab: {
...commonScreenOptions,
- cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator(isSmallScreenWidth, false, props),
+ cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator(isSmallScreenWidth, false, false, props),
cardStyle: {
...StyleUtils.getNavigationModalCardStyle(),
diff --git a/src/libs/Navigation/dismissModal.ts b/src/libs/Navigation/dismissModal.ts
index 19007836255d..4e159f0284bd 100644
--- a/src/libs/Navigation/dismissModal.ts
+++ b/src/libs/Navigation/dismissModal.ts
@@ -9,8 +9,6 @@ import type {RootStackParamList} from './types';
/**
* Dismisses the last modal stack if there is any
- *
- * @param targetReportID - The reportID to navigate to after dismissing the modal
*/
function dismissModal(navigationRef: NavigationContainerRef) {
if (!navigationRef.isReady()) {
@@ -23,6 +21,7 @@ function dismissModal(navigationRef: NavigationContainerRef)
case NAVIGATORS.FULL_SCREEN_NAVIGATOR:
case NAVIGATORS.LEFT_MODAL_NAVIGATOR:
case NAVIGATORS.RIGHT_MODAL_NAVIGATOR:
+ case NAVIGATORS.ONBOARDING_MODAL_NAVIGATOR:
case SCREENS.NOT_FOUND:
case SCREENS.REPORT_ATTACHMENTS:
case SCREENS.TRANSACTION_RECEIPT:
diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts
index c777a689b624..c9c3efbf2e16 100644
--- a/src/libs/Navigation/linkingConfig/config.ts
+++ b/src/libs/Navigation/linkingConfig/config.ts
@@ -80,6 +80,28 @@ const config: LinkingOptions['config'] = {
},
},
},
+ [NAVIGATORS.WELCOME_VIDEO_MODAL_NAVIGATOR]: {
+ screens: {
+ [SCREENS.WELCOME_VIDEO.ROOT]: {
+ path: ROUTES.WELCOME_VIDEO_ROOT,
+ exact: true,
+ },
+ },
+ },
+ [NAVIGATORS.ONBOARDING_MODAL_NAVIGATOR]: {
+ path: ROUTES.ONBOARDING_ROOT,
+ initialRouteName: SCREENS.ONBOARDING.PERSONAL_DETAILS,
+ screens: {
+ [SCREENS.ONBOARDING.PERSONAL_DETAILS]: {
+ path: ROUTES.ONBOARDING_PERSONAL_DETAILS,
+ exact: true,
+ },
+ [SCREENS.ONBOARDING.PURPOSE]: {
+ path: ROUTES.ONBOARDING_PURPOSE,
+ exact: true,
+ },
+ },
+ },
[NAVIGATORS.RIGHT_MODAL_NAVIGATOR]: {
screens: {
[SCREENS.RIGHT_MODAL.SETTINGS]: {
diff --git a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts
index 3cdcfba59466..07d2fac3b2e1 100644
--- a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts
+++ b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts
@@ -157,6 +157,8 @@ function getAdaptedState(state: PartialState
const fullScreenNavigator = state.routes.find((route) => route.name === NAVIGATORS.FULL_SCREEN_NAVIGATOR);
const rhpNavigator = state.routes.find((route) => route.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR);
const lhpNavigator = state.routes.find((route) => route.name === NAVIGATORS.LEFT_MODAL_NAVIGATOR);
+ const onboardingModalNavigator = state.routes.find((route) => route.name === NAVIGATORS.ONBOARDING_MODAL_NAVIGATOR);
+ const welcomeVideoModalNavigator = state.routes.find((route) => route.name === NAVIGATORS.WELCOME_VIDEO_MODAL_NAVIGATOR);
const reportAttachmentsScreen = state.routes.find((route) => route.name === SCREENS.REPORT_ATTACHMENTS);
if (isNarrowLayout) {
@@ -203,13 +205,13 @@ function getAdaptedState(state: PartialState
metainfo,
};
}
- if (lhpNavigator) {
+ if (lhpNavigator ?? onboardingModalNavigator ?? welcomeVideoModalNavigator) {
// Routes
// - default bottom tab
// - default central pane on desktop layout
- // - found lhp
+ // - found lhp / onboardingModalNavigator
- // Currently there is only the search and workspace switcher in LHP both can have any central pane under the overlay.
+ // There is no screen in these navigators that would have mandatory central pane, bottom tab or fullscreen navigator.
metainfo.isCentralPaneAndBottomTabMandatory = false;
metainfo.isFullScreenNavigatorMandatory = false;
const routes = [];
@@ -228,7 +230,19 @@ function getAdaptedState(state: PartialState
}),
);
}
- routes.push(lhpNavigator);
+
+ // Separate ifs are necessary for typescript to see that we are not pushing undefined to the array.
+ if (lhpNavigator) {
+ routes.push(lhpNavigator);
+ }
+
+ if (onboardingModalNavigator) {
+ routes.push(onboardingModalNavigator);
+ }
+
+ if (welcomeVideoModalNavigator) {
+ routes.push(welcomeVideoModalNavigator);
+ }
return {
adaptedState: getRoutesWithIndex(routes),
@@ -313,6 +327,10 @@ function getAdaptedState(state: PartialState
const matchingCentralPaneRoute = getMatchingCentralPaneRouteForState(state);
if (matchingCentralPaneRoute) {
routes.push(createCentralPaneNavigator(matchingCentralPaneRoute));
+ } else {
+ // If there is no matching central pane, we want to add the default one.
+ metainfo.isCentralPaneAndBottomTabMandatory = false;
+ routes.push(createCentralPaneNavigator({name: SCREENS.REPORT}));
}
return {
@@ -340,6 +358,7 @@ const getAdaptedStateFromPath: GetAdaptedStateFromPath = (path, options) => {
if (state === undefined) {
throw new Error('Unable to parse path');
}
+
return getAdaptedState(state, policyID);
};
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index ee1adaf73e46..fefad04b9faf 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -634,6 +634,16 @@ type FullScreenNavigatorParamList = {
[SCREENS.WORKSPACES_CENTRAL_PANE]: NavigatorScreenParams;
};
+type OnboardingModalNavigatorParamList = {
+ [SCREENS.ONBOARDING_MODAL.ONBOARDING]: undefined;
+ [SCREENS.ONBOARDING.PERSONAL_DETAILS]: undefined;
+ [SCREENS.ONBOARDING.PURPOSE]: undefined;
+};
+
+type WelcomeVideoModalNavigatorParamList = {
+ [SCREENS.WELCOME_VIDEO.ROOT]: undefined;
+};
+
type BottomTabNavigatorParamList = {
[SCREENS.HOME]: undefined;
[SCREENS.SETTINGS.ROOT]: undefined;
@@ -693,6 +703,8 @@ type AuthScreensParamList = SharedScreensParamList & {
[NAVIGATORS.LEFT_MODAL_NAVIGATOR]: NavigatorScreenParams;
[NAVIGATORS.RIGHT_MODAL_NAVIGATOR]: NavigatorScreenParams;
[NAVIGATORS.FULL_SCREEN_NAVIGATOR]: NavigatorScreenParams;
+ [NAVIGATORS.ONBOARDING_MODAL_NAVIGATOR]: NavigatorScreenParams;
+ [NAVIGATORS.WELCOME_VIDEO_MODAL_NAVIGATOR]: NavigatorScreenParams;
[SCREENS.DESKTOP_SIGN_IN_REDIRECT]: undefined;
[SCREENS.TRANSACTION_RECEIPT]: {
reportID: string;
@@ -730,6 +742,8 @@ export type {
BottomTabNavigatorParamList,
LeftModalNavigatorParamList,
RightModalNavigatorParamList,
+ OnboardingModalNavigatorParamList,
+ WelcomeVideoModalNavigatorParamList,
PublicScreensParamList,
MoneyRequestNavigatorParamList,
SplitDetailsNavigatorParamList,
diff --git a/src/libs/actions/PersonalDetails.ts b/src/libs/actions/PersonalDetails.ts
index 20323020aa0b..085d8b78e418 100644
--- a/src/libs/actions/PersonalDetails.ts
+++ b/src/libs/actions/PersonalDetails.ts
@@ -72,30 +72,30 @@ function updatePronouns(pronouns: string) {
}
function updateDisplayName(firstName: string, lastName: string) {
- if (currentUserAccountID) {
- const parameters: UpdateDisplayNameParams = {firstName, lastName};
+ if (!currentUserAccountID) {
+ return;
+ }
- API.write(WRITE_COMMANDS.UPDATE_DISPLAY_NAME, parameters, {
- optimisticData: [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: ONYXKEYS.PERSONAL_DETAILS_LIST,
- value: {
- [currentUserAccountID]: {
+ const parameters: UpdateDisplayNameParams = {firstName, lastName};
+
+ API.write(WRITE_COMMANDS.UPDATE_DISPLAY_NAME, parameters, {
+ optimisticData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.PERSONAL_DETAILS_LIST,
+ value: {
+ [currentUserAccountID]: {
+ firstName,
+ lastName,
+ displayName: PersonalDetailsUtils.createDisplayName(currentUserEmail ?? '', {
firstName,
lastName,
- displayName: PersonalDetailsUtils.createDisplayName(currentUserEmail ?? '', {
- firstName,
- lastName,
- }),
- },
+ }),
},
},
- ],
- });
- }
-
- Navigation.goBack();
+ },
+ ],
+ });
}
function updateLegalName(legalFirstName: string, legalLastName: string) {
diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts
index b2c361525600..3605f8c39962 100644
--- a/src/libs/actions/Report.ts
+++ b/src/libs/actions/Report.ts
@@ -1769,7 +1769,7 @@ function navigateToConciergeChat(shouldDismissModal = false, checkIfCurrentPageA
if (!conciergeChatReportID) {
// In order to avoid creating concierge repeatedly,
// we need to ensure that the server data has been successfully pulled
- Welcome.serverDataIsReadyPromise().then(() => {
+ Welcome.onServerDataReady().then(() => {
// If we don't have a chat with Concierge then create it
if (!checkIfCurrentPageActive()) {
return;
@@ -2721,7 +2721,7 @@ function getReportPrivateNote(reportID: string | undefined) {
* - Sets the introSelected NVP to the choice the user made
* - Creates an optimistic report comment from concierge
*/
-function completeEngagementModal(text: string, choice: ValueOf) {
+function completeEngagementModal(text: string, choice: ValueOf) {
const conciergeAccountID = PersonalDetailsUtils.getAccountIDsByLogins([CONST.EMAIL.CONCIERGE])[0];
const reportComment = ReportUtils.buildOptimisticAddCommentReportAction(text, undefined, conciergeAccountID);
const reportCommentAction: OptimisticAddCommentReportAction = reportComment.reportAction;
diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts
index 798fc2886fa8..17004baef43e 100644
--- a/src/libs/actions/Session/index.ts
+++ b/src/libs/actions/Session/index.ts
@@ -39,7 +39,6 @@ import * as Device from '@userActions/Device';
import * as PriorityMode from '@userActions/PriorityMode';
import redirectToSignIn from '@userActions/SignInRedirect';
import Timing from '@userActions/Timing';
-import * as Welcome from '@userActions/Welcome';
import CONFIG from '@src/CONFIG';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
@@ -643,7 +642,6 @@ function resetHomeRouteParams() {
function cleanupSession() {
Pusher.disconnect();
Timers.clearAll();
- Welcome.resetReadyCheck();
PriorityMode.resetHasReadRequiredDataFromStorage();
MainQueue.clear();
HttpUtils.cancelPendingRequests();
diff --git a/src/libs/actions/Welcome.ts b/src/libs/actions/Welcome.ts
index 150b23a4a0d9..fc72733b190c 100644
--- a/src/libs/actions/Welcome.ts
+++ b/src/libs/actions/Welcome.ts
@@ -1,29 +1,85 @@
-import type {NavigationState} from '@react-navigation/native';
import type {OnyxCollection} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
-import Navigation from '@libs/Navigation/Navigation';
-import * as ReportUtils from '@libs/ReportUtils';
-import type {RootStackParamList, State} from '@navigation/types';
-import CONST from '@src/CONST';
+import type {SelectedPurposeType} from '@pages/OnboardingPurpose/BaseOnboardingPurpose';
import ONYXKEYS from '@src/ONYXKEYS';
-import ROUTES from '@src/ROUTES';
-import SCREENS from '@src/SCREENS';
import type OnyxPolicy from '@src/types/onyx/Policy';
-import type Report from '@src/types/onyx/Report';
import type {EmptyObject} from '@src/types/utils/EmptyObject';
-import * as Policy from './Policy';
-import * as Session from './Session';
-
-let resolveIsReadyPromise: (value?: Promise) => void | undefined;
-let isReadyPromise = new Promise((resolve) => {
- resolveIsReadyPromise = resolve;
-});
+let hasSelectedPurpose: boolean | undefined;
+let hasProvidedPersonalDetails: boolean | undefined;
let isFirstTimeNewExpensifyUser: boolean | undefined;
let hasDismissedModal: boolean | undefined;
let hasSelectedChoice: boolean | undefined;
let isLoadingReportData = true;
-let currentUserAccountID: number | undefined;
+
+type DetermineOnboardingStatusProps = {
+ onAble?: () => void;
+ onNotAble?: () => void;
+};
+
+type HasCompletedOnboardingFlowProps = {
+ onCompleted?: () => void;
+ onNotCompleted?: () => void;
+};
+
+let resolveIsReadyPromise: (value?: Promise) => void | undefined;
+const isServerDataReadyPromise = new Promise((resolve) => {
+ resolveIsReadyPromise = resolve;
+});
+
+let resolveOnboardingFlowStatus: (value?: Promise) => void | undefined;
+const isOnboardingFlowStatusKnownPromise = new Promise((resolve) => {
+ resolveOnboardingFlowStatus = resolve;
+});
+
+function onServerDataReady(): Promise {
+ return isServerDataReadyPromise;
+}
+
+/**
+ * Checks if Onyx keys required to determine the
+ * onboarding flow status have been loaded (namely,
+ * are not undefined).
+ */
+function isAbleToDetermineOnboardingStatus({onAble, onNotAble}: DetermineOnboardingStatusProps) {
+ const hasRequiredOnyxKeysBeenLoaded = [hasProvidedPersonalDetails, hasSelectedPurpose].every((value) => value !== undefined);
+
+ if (hasRequiredOnyxKeysBeenLoaded) {
+ onAble?.();
+ } else {
+ onNotAble?.();
+ }
+}
+
+/**
+ * A promise returning the onboarding flow status.
+ * Returns true if user has completed the onboarding
+ * flow, false otherwise.
+ */
+function isOnboardingFlowCompleted({onCompleted, onNotCompleted}: HasCompletedOnboardingFlowProps) {
+ isOnboardingFlowStatusKnownPromise.then(() => {
+ // Remove once Stage 1 Onboarding Flow is ready
+ if (!isFirstTimeNewExpensifyUser) {
+ return;
+ }
+
+ // Uncomment once Stage 1 Onboarding Flow is ready
+ //
+ // const onboardingFlowCompleted = hasProvidedPersonalDetails && hasSelectedPurpose;
+ //
+ const onboardingFlowCompleted = hasSelectedPurpose;
+
+ if (onboardingFlowCompleted) {
+ onCompleted?.();
+ } else {
+ // Remove once Stage 1 Onboarding Flow is ready
+ // This key is only updated when we call ReconnectApp, setting it to false now allows the user to navigate normally instead of always redirecting to the workspace chat
+ Onyx.set(ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER, false);
+
+ onNotCompleted?.();
+ }
+ });
+}
/**
* Check that a few requests have completed so that the welcome action can proceed:
@@ -33,13 +89,34 @@ let currentUserAccountID: number | undefined;
* - Whether we have loaded all reports the server knows about
*/
function checkOnReady() {
- if (isFirstTimeNewExpensifyUser === undefined || isLoadingReportData || hasSelectedChoice === undefined || hasDismissedModal === undefined) {
+ const hasRequiredOnyxKeysBeenLoaded = [isFirstTimeNewExpensifyUser, hasSelectedChoice, hasDismissedModal].every((value) => value !== undefined);
+
+ if (isLoadingReportData || !hasRequiredOnyxKeysBeenLoaded) {
return;
}
resolveIsReadyPromise?.();
}
+function getPersonalDetails(accountID: number | undefined) {
+ Onyx.connect({
+ key: ONYXKEYS.PERSONAL_DETAILS_LIST,
+ initWithStoredValues: true,
+ callback: (value) => {
+ if (!value || !accountID) {
+ return;
+ }
+
+ hasProvidedPersonalDetails = !!value[accountID]?.firstName && !!value[accountID]?.lastName;
+ isAbleToDetermineOnboardingStatus({onAble: resolveOnboardingFlowStatus});
+ },
+ });
+}
+
+function setOnboardingPurposeSelected(value: SelectedPurposeType) {
+ Onyx.set(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, value ?? null);
+}
+
Onyx.connect({
key: ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER,
initWithStoredValues: false,
@@ -57,9 +134,8 @@ Onyx.connect({
key: ONYXKEYS.NVP_INTRO_SELECTED,
initWithStoredValues: true,
callback: (value) => {
- hasSelectedChoice = !!value;
-
- checkOnReady();
+ hasSelectedPurpose = !!value;
+ isAbleToDetermineOnboardingStatus({onAble: resolveOnboardingFlowStatus});
},
});
@@ -82,16 +158,6 @@ Onyx.connect({
},
});
-let allReports: OnyxCollection = {};
-Onyx.connect({
- key: ONYXKEYS.COLLECTION.REPORT,
- initWithStoredValues: false,
- waitForCollectionCallback: true,
- callback: (reports) => {
- allReports = reports;
- },
-});
-
const allPolicies: OnyxCollection | EmptyObject = {};
Onyx.connect({
key: ONYXKEYS.COLLECTION.POLICY,
@@ -116,82 +182,8 @@ Onyx.connect({
return;
}
- currentUserAccountID = val.accountID;
+ getPersonalDetails(val.accountID);
},
});
-/**
- * Shows a welcome action on first login
- */
-function show(routes: State['routes'] | undefined, showEngagementModal = () => {}) {
- isReadyPromise.then(() => {
- if (!isFirstTimeNewExpensifyUser) {
- return;
- }
-
- // If we are rendering the SidebarScreen at the same time as a workspace route that means we've already created a workspace via workspace/new and should not open the global
- // create menu right now. We should also stay on the workspace page if that is our destination.
- const transitionRoute = routes?.find(
- (route): route is NavigationState>['routes'][number] => route.name === SCREENS.TRANSITION_BETWEEN_APPS,
- );
- const isExitingToWorkspaceRoute = transitionRoute?.params?.exitTo === 'workspace/new';
-
- // If we already opened the workspace settings or want the admin room to stay open, do not
- // navigate away to the workspace chat report
- const shouldNavigateToWorkspaceChat = !isExitingToWorkspaceRoute;
-
- const workspaceChatReport = Object.values(allReports ?? {}).find((report) => {
- if (report) {
- return ReportUtils.isPolicyExpenseChat(report) && report.ownerAccountID === currentUserAccountID && report.statusNum !== CONST.REPORT.STATUS_NUM.CLOSED;
- }
- return false;
- });
-
- if (workspaceChatReport) {
- // This key is only updated when we call ReconnectApp, setting it to false now allows the user to navigate normally instead of always redirecting to the workspace chat
- Onyx.set(ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER, false);
- }
-
- if (shouldNavigateToWorkspaceChat && workspaceChatReport) {
- if (workspaceChatReport.reportID !== null) {
- Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(workspaceChatReport.reportID));
- }
-
- // New user has been redirected to their workspace chat, and we won't show them the engagement modal.
- // So we update isFirstTimeNewExpensifyUser to prevent the Welcome logic from running again
- isFirstTimeNewExpensifyUser = false;
-
- return;
- }
-
- // If user is not already an admin of a free policy and we are not navigating them to their workspace or creating a new workspace via workspace/new then
- // we will show the engagement modal.
- if (
- !Session.isAnonymousUser() &&
- !Policy.isAdminOfFreePolicy(allPolicies ?? undefined) &&
- !isExitingToWorkspaceRoute &&
- !hasSelectedChoice &&
- !hasDismissedModal &&
- Object.keys(allPolicies ?? {}).length === 1
- ) {
- showEngagementModal();
- }
-
- // Update isFirstTimeNewExpensifyUser so the Welcome logic doesn't run again
- isFirstTimeNewExpensifyUser = false;
- });
-}
-
-function resetReadyCheck() {
- isReadyPromise = new Promise((resolve) => {
- resolveIsReadyPromise = resolve;
- });
- isFirstTimeNewExpensifyUser = undefined;
- isLoadingReportData = true;
-}
-
-function serverDataIsReadyPromise(): Promise {
- return isReadyPromise;
-}
-
-export {show, serverDataIsReadyPromise, resetReadyCheck};
+export {onServerDataReady, isOnboardingFlowCompleted, setOnboardingPurposeSelected};
diff --git a/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx b/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx
new file mode 100644
index 000000000000..f49c5c0f506e
--- /dev/null
+++ b/src/pages/OnboardingPersonalDetails/BaseOnboardingPersonalDetails.tsx
@@ -0,0 +1,146 @@
+import React, {useCallback} from 'react';
+import {View} from 'react-native';
+import FormProvider from '@components/Form/FormProvider';
+import InputWrapper from '@components/Form/InputWrapper';
+import type {FormOnyxValues} from '@components/Form/types';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import KeyboardAvoidingView from '@components/KeyboardAvoidingView';
+import OfflineIndicator from '@components/OfflineIndicator';
+import Text from '@components/Text';
+import TextInput from '@components/TextInput';
+import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails';
+import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails';
+import useLocalize from '@hooks/useLocalize';
+import useOnboardingLayout from '@hooks/useOnboardingLayout';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import useWindowDimensions from '@hooks/useWindowDimensions';
+import * as ErrorUtils from '@libs/ErrorUtils';
+import Navigation from '@libs/Navigation/Navigation';
+import * as ValidationUtils from '@libs/ValidationUtils';
+import * as PersonalDetails from '@userActions/PersonalDetails';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import INPUT_IDS from '@src/types/form/DisplayNameForm';
+
+type BaseOnboardingPersonalDetailsProps = {
+ /* Whether to use native styles tailored for native devices */
+ shouldUseNativeStyles: boolean;
+};
+
+function BaseOnboardingPersonalDetails({currentUserPersonalDetails, shouldUseNativeStyles}: WithCurrentUserPersonalDetailsProps & BaseOnboardingPersonalDetailsProps) {
+ const theme = useTheme();
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const {isSmallScreenWidth} = useWindowDimensions();
+ const {shouldUseNarrowLayout} = useOnboardingLayout();
+
+ const saveAndNavigate = useCallback((values: FormOnyxValues<'onboardingPersonalDetailsForm'>) => {
+ PersonalDetails.updateDisplayName(values.firstName.trim(), values.lastName.trim());
+
+ Navigation.navigate(ROUTES.ONBOARDING_PURPOSE);
+ }, []);
+
+ const validate = (values: FormOnyxValues<'onboardingPersonalDetailsForm'>) => {
+ const errors = {};
+
+ // First we validate the first name field
+ if (values.firstName.length === 0) {
+ ErrorUtils.addErrorMessage(errors, 'firstName', 'onboarding.error.requiredFirstName');
+ }
+ if (!ValidationUtils.isValidDisplayName(values.firstName)) {
+ ErrorUtils.addErrorMessage(errors, 'firstName', 'personalDetails.error.hasInvalidCharacter');
+ } else if (values.firstName.length > CONST.DISPLAY_NAME.MAX_LENGTH) {
+ ErrorUtils.addErrorMessage(errors, 'firstName', ['common.error.characterLimitExceedCounter', {length: values.firstName.length, limit: CONST.DISPLAY_NAME.MAX_LENGTH}]);
+ }
+ if (ValidationUtils.doesContainReservedWord(values.firstName, CONST.DISPLAY_NAME.RESERVED_NAMES)) {
+ ErrorUtils.addErrorMessage(errors, 'firstName', 'personalDetails.error.containsReservedWord');
+ }
+
+ // Then we validate the last name field
+ if (values.lastName.length === 0) {
+ ErrorUtils.addErrorMessage(errors, 'lastName', 'onboarding.error.requiredLastName');
+ }
+ if (!ValidationUtils.isValidDisplayName(values.lastName)) {
+ ErrorUtils.addErrorMessage(errors, 'lastName', 'personalDetails.error.hasInvalidCharacter');
+ } else if (values.lastName.length > CONST.DISPLAY_NAME.MAX_LENGTH) {
+ ErrorUtils.addErrorMessage(errors, 'lastName', ['common.error.characterLimitExceedCounter', {length: values.lastName.length, limit: CONST.DISPLAY_NAME.MAX_LENGTH}]);
+ }
+ if (ValidationUtils.doesContainReservedWord(values.lastName, CONST.DISPLAY_NAME.RESERVED_NAMES)) {
+ ErrorUtils.addErrorMessage(errors, 'lastName', 'personalDetails.error.containsReservedWord');
+ }
+
+ return errors;
+ };
+
+ const PersonalDetailsFooterInstance = ;
+
+ return (
+
+
+
+
+
+ {translate('onboarding.welcome')}
+ {translate('onboarding.whatsYourName')}
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+BaseOnboardingPersonalDetails.displayName = 'BaseOnboardingPersonalDetails';
+
+export default withCurrentUserPersonalDetails(BaseOnboardingPersonalDetails);
+
+export type {BaseOnboardingPersonalDetailsProps};
diff --git a/src/pages/OnboardingPersonalDetails/index.native.tsx b/src/pages/OnboardingPersonalDetails/index.native.tsx
new file mode 100644
index 000000000000..3c49a13178e6
--- /dev/null
+++ b/src/pages/OnboardingPersonalDetails/index.native.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import type {BaseOnboardingPersonalDetailsProps} from './BaseOnboardingPersonalDetails';
+import BaseOnboardingPersonalDetails from './BaseOnboardingPersonalDetails';
+
+function OnboardingPersonalDetails({...rest}: Omit) {
+ return (
+
+ );
+}
+
+OnboardingPersonalDetails.displayName = 'OnboardingPurpose';
+
+export default OnboardingPersonalDetails;
diff --git a/src/pages/OnboardingPersonalDetails/index.tsx b/src/pages/OnboardingPersonalDetails/index.tsx
new file mode 100644
index 000000000000..429caa466e35
--- /dev/null
+++ b/src/pages/OnboardingPersonalDetails/index.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import type {BaseOnboardingPersonalDetailsProps} from './BaseOnboardingPersonalDetails';
+import BaseOnboardingPersonalDetails from './BaseOnboardingPersonalDetails';
+
+function OnboardingPersonalDetails({...rest}: Omit) {
+ return (
+
+ );
+}
+
+OnboardingPersonalDetails.displayName = 'OnboardingPurpose';
+
+export default OnboardingPersonalDetails;
diff --git a/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx b/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx
new file mode 100644
index 000000000000..ae43f850018b
--- /dev/null
+++ b/src/pages/OnboardingPurpose/BaseOnboardingPurpose.tsx
@@ -0,0 +1,175 @@
+import React, {useCallback, useEffect, useMemo, useState} from 'react';
+import {View} from 'react-native';
+import {ScrollView} from 'react-native-gesture-handler';
+import {withOnyx} from 'react-native-onyx';
+import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import Icon from '@components/Icon';
+import * as Expensicons from '@components/Icon/Expensicons';
+import * as Illustrations from '@components/Icon/Illustrations';
+import type {MenuItemProps} from '@components/MenuItem';
+import MenuItemList from '@components/MenuItemList';
+import OfflineIndicator from '@components/OfflineIndicator';
+import SafeAreaConsumer from '@components/SafeAreaConsumer';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useOnboardingLayout from '@hooks/useOnboardingLayout';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import useWindowDimensions from '@hooks/useWindowDimensions';
+import Navigation from '@libs/Navigation/Navigation';
+import variables from '@styles/variables';
+import * as Report from '@userActions/Report';
+import * as Welcome from '@userActions/Welcome';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type {BaseOnboardingPurposeOnyxProps, BaseOnboardingPurposeProps} from './types';
+
+type ValuesType = T[keyof T];
+type SelectedPurposeType = ValuesType | undefined;
+
+const menuIcons = {
+ [CONST.ONBOARDING_CHOICES.TRACK]: Illustrations.CompanyCard,
+ [CONST.ONBOARDING_CHOICES.EMPLOYER]: Illustrations.ReceiptUpload,
+ [CONST.ONBOARDING_CHOICES.MANAGE_TEAM]: Illustrations.Abacus,
+ [CONST.ONBOARDING_CHOICES.PERSONAL_SPEND]: Illustrations.PiggyBank,
+ [CONST.ONBOARDING_CHOICES.CHAT_SPLIT]: Illustrations.SplitBill,
+ [CONST.ONBOARDING_CHOICES.LOOKING_AROUND]: Illustrations.Binoculars,
+};
+
+function BaseOnboardingPurpose({shouldUseNativeStyles, shouldEnableMaxHeight, onboardingPurposeSelected}: BaseOnboardingPurposeProps) {
+ const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const {shouldUseNarrowLayout} = useOnboardingLayout();
+ const [selectedPurpose, setSelectedPurpose] = useState(undefined);
+ const {isSmallScreenWidth, windowHeight} = useWindowDimensions();
+ const [error, setError] = useState(false);
+ const theme = useTheme();
+
+ const PurposeFooterInstance = ;
+
+ useEffect(() => {
+ setSelectedPurpose(onboardingPurposeSelected ?? undefined);
+ }, [onboardingPurposeSelected]);
+
+ const errorMessage = error ? 'onboarding.purpose.error' : '';
+
+ const maxHeight = shouldEnableMaxHeight ? windowHeight : undefined;
+
+ const paddingHorizontal = shouldUseNarrowLayout ? styles.ph8 : styles.ph5;
+
+ const handleGoBack = useCallback(() => {
+ Navigation.goBack();
+ }, []);
+
+ const selectedCheckboxIcon = useMemo(
+ () => (
+
+
+
+ ),
+ [styles.pointerEventsAuto, styles.popoverMenuIcon, theme.success],
+ );
+
+ const completeEngagement = useCallback(() => {
+ if (selectedPurpose === undefined) {
+ return;
+ }
+
+ Report.completeEngagementModal(CONST.ONBOARDING_CONCIERGE[selectedPurpose], selectedPurpose);
+
+ Navigation.dismissModal();
+ // Only navigate to concierge chat when central pane is visible
+ // Otherwise stay on the chats screen.
+ if (isSmallScreenWidth) {
+ Navigation.navigate(ROUTES.HOME);
+ } else {
+ Report.navigateToConciergeChat();
+ }
+
+ // Small delay purely due to design considerations,
+ // no special technical reasons behind that.
+ setTimeout(() => {
+ Navigation.navigate(ROUTES.WELCOME_VIDEO_ROOT);
+ }, variables.welcomeVideoDelay);
+ }, [isSmallScreenWidth, selectedPurpose]);
+
+ const menuItems: MenuItemProps[] = Object.values(CONST.ONBOARDING_CHOICES).map((choice) => {
+ const translationKey = `onboarding.purpose.${choice}` as const;
+ const isSelected = selectedPurpose === choice;
+ return {
+ key: translationKey,
+ title: translate(translationKey),
+ icon: menuIcons[choice],
+ displayInDefaultIconColor: true,
+ iconWidth: variables.purposeMenuIconSize,
+ iconHeight: variables.purposeMenuIconSize,
+ iconStyles: [styles.mh3],
+ wrapperStyle: [styles.purposeMenuItem, isSelected && styles.purposeMenuItemSelected],
+ hoverAndPressStyle: [styles.purposeMenuItemSelected],
+ rightComponent: selectedCheckboxIcon,
+ shouldShowRightComponent: isSelected,
+ onPress: () => {
+ Welcome.setOnboardingPurposeSelected(choice);
+ setError(false);
+ },
+ };
+ });
+
+ return (
+
+ {({safeAreaPaddingBottomStyle}) => (
+
+
+
+
+
+
+
+ {translate('onboarding.purpose.title')}
+
+
+
+
+ {
+ if (!selectedPurpose) {
+ setError(true);
+ return;
+ }
+ setError(false);
+ completeEngagement();
+ }}
+ message={errorMessage}
+ isAlertVisible={error || Boolean(errorMessage)}
+ containerStyles={[styles.w100, styles.mb5, styles.mh0, paddingHorizontal]}
+ />
+
+ )}
+
+ );
+}
+
+BaseOnboardingPurpose.displayName = 'BaseOnboardingPurpose';
+
+export default withOnyx({
+ onboardingPurposeSelected: {
+ key: ONYXKEYS.ONBOARDING_PURPOSE_SELECTED,
+ },
+})(BaseOnboardingPurpose);
+
+export type {BaseOnboardingPurposeProps, SelectedPurposeType};
diff --git a/src/pages/OnboardingPurpose/index.native.tsx b/src/pages/OnboardingPurpose/index.native.tsx
new file mode 100644
index 000000000000..c13245038629
--- /dev/null
+++ b/src/pages/OnboardingPurpose/index.native.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import BaseOnboardingPurpose from './BaseOnboardingPurpose';
+import type {OnboardingPurposeProps} from './types';
+
+function OnboardingPurpose({...rest}: OnboardingPurposeProps) {
+ return (
+
+ );
+}
+
+OnboardingPurpose.displayName = 'OnboardingPurpose';
+export default OnboardingPurpose;
diff --git a/src/pages/OnboardingPurpose/index.tsx b/src/pages/OnboardingPurpose/index.tsx
new file mode 100644
index 000000000000..d7abedacbb85
--- /dev/null
+++ b/src/pages/OnboardingPurpose/index.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import BaseOnboardingPurpose from './BaseOnboardingPurpose';
+import type {OnboardingPurposeProps} from './types';
+
+function OnboardingPurpose({...rest}: OnboardingPurposeProps) {
+ return (
+
+ );
+}
+
+OnboardingPurpose.displayName = 'OnboardingPurpose';
+export default OnboardingPurpose;
diff --git a/src/pages/OnboardingPurpose/index.website.tsx b/src/pages/OnboardingPurpose/index.website.tsx
new file mode 100644
index 000000000000..29252c9010f6
--- /dev/null
+++ b/src/pages/OnboardingPurpose/index.website.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import BaseOnboardingPurpose from './BaseOnboardingPurpose';
+import type {OnboardingPurposeProps} from './types';
+
+function OnboardingPurpose({...rest}: OnboardingPurposeProps) {
+ return (
+
+ );
+}
+
+OnboardingPurpose.displayName = 'OnboardingPurpose';
+export default OnboardingPurpose;
diff --git a/src/pages/OnboardingPurpose/types.ts b/src/pages/OnboardingPurpose/types.ts
new file mode 100644
index 000000000000..586463a26bb0
--- /dev/null
+++ b/src/pages/OnboardingPurpose/types.ts
@@ -0,0 +1,19 @@
+import type {OnyxEntry} from 'react-native-onyx';
+
+type OnboardingPurposeProps = Record;
+
+type BaseOnboardingPurposeOnyxProps = {
+ /** Saved onboarding purpose selected by the user */
+ onboardingPurposeSelected: OnyxEntry;
+};
+
+type BaseOnboardingPurposeProps = OnboardingPurposeProps &
+ BaseOnboardingPurposeOnyxProps & {
+ /* Whether to use native styles tailored for native devices */
+ shouldUseNativeStyles: boolean;
+
+ /** Whether to use the maxHeight (true) or use the 100% of the height (false) */
+ shouldEnableMaxHeight: boolean;
+ };
+
+export type {BaseOnboardingPurposeOnyxProps, BaseOnboardingPurposeProps, OnboardingPurposeProps};
diff --git a/src/pages/settings/Profile/DisplayNamePage.tsx b/src/pages/settings/Profile/DisplayNamePage.tsx
index 2e7a67509139..3ab443b23561 100644
--- a/src/pages/settings/Profile/DisplayNamePage.tsx
+++ b/src/pages/settings/Profile/DisplayNamePage.tsx
@@ -33,6 +33,7 @@ type DisplayNamePageProps = DisplayNamePageOnyxProps & WithCurrentUserPersonalDe
*/
const updateDisplayName = (values: FormOnyxValues) => {
PersonalDetails.updateDisplayName(values.firstName.trim(), values.lastName.trim());
+ Navigation.goBack();
};
function DisplayNamePage({isLoadingApp = true, currentUserPersonalDetails}: DisplayNamePageProps) {
diff --git a/src/stories/HeaderWithBackButton.stories.tsx b/src/stories/HeaderWithBackButton.stories.tsx
index ca723715d5f0..1705c6874c82 100644
--- a/src/stories/HeaderWithBackButton.stories.tsx
+++ b/src/stories/HeaderWithBackButton.stories.tsx
@@ -25,6 +25,7 @@ function Template(props: HeaderWithBackButtonProps) {
const Default: HeaderWithBackButtonStory = Template.bind({});
const Attachment: HeaderWithBackButtonStory = Template.bind({});
const Profile: HeaderWithBackButtonStory = Template.bind({});
+const ProgressBar: HeaderWithBackButtonStory = Template.bind({});
Default.args = {
title: 'Settings',
};
@@ -34,8 +35,12 @@ Attachment.args = {
};
Profile.args = {
title: 'Profile',
- shouldShowBackButton: true,
+};
+ProgressBar.args = {
+ title: 'ProgressBar',
+ progressBarPercentage: 33,
+ shouldShowBackButton: false,
};
export default story;
-export {Default, Attachment, Profile};
+export {Default, Attachment, Profile, ProgressBar};
diff --git a/src/styles/index.ts b/src/styles/index.ts
index a736bc537fa6..f165974119ff 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -1571,6 +1571,25 @@ const styles = (theme: ThemeColors) =>
right: 0,
} satisfies ViewStyle),
+ onboardingNavigatorOuterView: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ },
+
+ OnboardingNavigatorInnerView: (shouldUseNarrowLayout: boolean) =>
+ ({
+ width: shouldUseNarrowLayout ? variables.onboardingModalWidth : '100%',
+ height: shouldUseNarrowLayout ? 732 : '100%',
+ maxHeight: '100%',
+ borderRadius: shouldUseNarrowLayout ? 16 : 0,
+ overflow: 'hidden',
+ } satisfies ViewStyle),
+
+ welcomeVideoNarrowLayout: {
+ width: variables.onboardingModalWidth,
+ },
+
onlyEmojisText: {
fontSize: variables.fontSizeOnlyEmojis,
lineHeight: variables.fontSizeOnlyEmojisHeight,
@@ -1805,6 +1824,19 @@ const styles = (theme: ThemeColors) =>
}),
} satisfies ViewStyle),
+ nativeOverlayStyles: (current: OverlayStylesParams) =>
+ ({
+ position: 'absolute',
+ backgroundColor: theme.overlay,
+ width: '100%',
+ height: '100%',
+ opacity: current.progress.interpolate({
+ inputRange: [0, 1],
+ outputRange: [0, variables.overlayOpacity],
+ extrapolate: 'clamp',
+ }),
+ } satisfies ViewStyle),
+
appContent: {
backgroundColor: theme.appBG,
overflow: 'hidden',
@@ -4029,6 +4061,18 @@ const styles = (theme: ThemeColors) =>
zIndex: -1,
},
+ purposeMenuItem: {
+ backgroundColor: theme.cardBG,
+ borderRadius: 8,
+ paddingHorizontal: 8,
+ alignItems: 'center',
+ marginBottom: 8,
+ },
+
+ purposeMenuItemSelected: {
+ backgroundColor: theme.activeComponentBG,
+ },
+
willChangeTransform: {
willChange: 'transform',
},
@@ -4208,6 +4252,11 @@ const styles = (theme: ThemeColors) =>
fontSize: 9,
},
+ onboardingVideoPlayer: {
+ borderRadius: 12,
+ backgroundColor: theme.highlightBG,
+ },
+
sidebarStatusAvatarContainer: {
height: 40,
width: 40,
@@ -4482,6 +4531,26 @@ const styles = (theme: ThemeColors) =>
borderColor: theme.icon,
},
+ headerProgressBarContainer: {
+ position: 'absolute',
+ width: '100%',
+ zIndex: -1,
+ },
+
+ headerProgressBar: {
+ width: variables.componentSizeMedium,
+ height: variables.iconSizeXXXSmall,
+ borderRadius: variables.componentBorderRadiusRounded,
+ backgroundColor: theme.border,
+ alignSelf: 'center',
+ },
+
+ headerProgressBarFill: {
+ borderRadius: variables.componentBorderRadiusRounded,
+ height: '100%',
+ backgroundColor: theme.success,
+ },
+
interactiveStepHeaderContainer: {
flex: 1,
alignSelf: 'center',
diff --git a/src/styles/utils/sizing.ts b/src/styles/utils/sizing.ts
index 1e9a6662d102..371860a59efc 100644
--- a/src/styles/utils/sizing.ts
+++ b/src/styles/utils/sizing.ts
@@ -30,6 +30,10 @@ export default {
width: '25%',
},
+ mh100: {
+ maxHeight: '100%',
+ },
+
mnh100: {
minHeight: '100%',
},
diff --git a/src/styles/variables.ts b/src/styles/variables.ts
index ac04a436f72e..309c90fc631e 100644
--- a/src/styles/variables.ts
+++ b/src/styles/variables.ts
@@ -202,6 +202,8 @@ export default {
oldDotWireframeIconHeight: 143.28,
sectionIllustrationHeight: 220,
photoUploadPopoverWidth: 335,
+ onboardingModalWidth: 500,
+ welcomeVideoDelay: 500,
// The height of the empty list is 14px (2px for borders and 12px for vertical padding)
// This is calculated based on the values specified in the 'getGoogleListViewStyle' function of the 'StyleUtils' utility
@@ -209,6 +211,7 @@ export default {
hoverDimValue: 1,
pressDimValue: 0.8,
qrShareHorizontalPadding: 32,
+ purposeMenuIconSize: 48,
moneyRequestSkeletonHeight: 107,
diff --git a/src/types/onyx/IntroSelected.ts b/src/types/onyx/IntroSelected.ts
index f0047ac134ee..9917f4b44550 100644
--- a/src/types/onyx/IntroSelected.ts
+++ b/src/types/onyx/IntroSelected.ts
@@ -3,7 +3,7 @@ import type CONST from '@src/CONST';
type IntroSelected = {
/** The choice that the user selected in the engagement modal */
- choice: ValueOf;
+ choice: ValueOf;
};
export default IntroSelected;