diff --git a/assets/images/simple-illustrations/simple-illustration__flash.svg b/assets/images/simple-illustrations/simple-illustration__flash.svg new file mode 100644 index 000000000000..be8daf296aa1 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__flash.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__mobileapp.svg b/assets/images/simple-illustrations/simple-illustration__mobileapp.svg new file mode 100644 index 000000000000..80682c942f81 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__mobileapp.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/CONST.ts b/src/CONST.ts index ff77a7380a6b..44b2fd16f64c 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -679,6 +679,7 @@ const CONST = { COMBINED_TRACK_SUBMIT: 'combinedTrackSubmit', CATEGORY_AND_TAG_APPROVERS: 'categoryAndTagApprovers', PER_DIEM: 'newDotPerDiem', + PRODUCT_TRAINING: 'productTraining', }, BUTTON_STATES: { DEFAULT: 'default', @@ -6391,6 +6392,8 @@ const CONST = { HYBRID_APP: { REORDERING_REACT_NATIVE_ACTIVITY_TO_FRONT: 'reorderingReactNativeActivityToFront', }, + + MIGRATED_USER_WELCOME_MODAL: 'migratedUserWelcomeModal', } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/NAVIGATORS.ts b/src/NAVIGATORS.ts index 0b4a86c99247..b7c7a71c2828 100644 --- a/src/NAVIGATORS.ts +++ b/src/NAVIGATORS.ts @@ -11,5 +11,6 @@ export default { FEATURE_TRANING_MODAL_NAVIGATOR: 'FeatureTrainingModalNavigator', WELCOME_VIDEO_MODAL_NAVIGATOR: 'WelcomeVideoModalNavigator', EXPLANATION_MODAL_NAVIGATOR: 'ExplanationModalNavigator', + MIGRATED_USER_MODAL_NAVIGATOR: 'MigratedUserModalNavigator', FULL_SCREEN_NAVIGATOR: 'FullScreenNavigator', } as const; diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 3c3812774380..45d636c0b1df 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -362,6 +362,9 @@ const ONYXKEYS = { // Stores onboarding last visited path ONBOARDING_LAST_VISITED_PATH: 'onboardingLastVisitedPath', + // Object containing names/timestamps of dismissed product training elements (Modal, Tooltip, etc.) + NVP_DISMISSED_PRODUCT_TRAINING: 'nvp_dismissedProductTraining', + // Max width supported for HTML element MAX_CANVAS_WIDTH: 'maxCanvasWidth', @@ -1027,6 +1030,7 @@ type OnyxValuesMapping = { [ONYXKEYS.SHOULD_SHOW_SAVED_SEARCH_RENAME_TOOLTIP]: boolean; [ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES]: Record; [ONYXKEYS.CONCIERGE_REPORT_ID]: string; + [ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING]: OnyxTypes.DismissedProductTraining; }; type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 4ffd576243a6..4abd5c6d3d49 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1387,6 +1387,7 @@ const ROUTES = { }, WELCOME_VIDEO_ROOT: 'onboarding/welcome-video', EXPLANATION_MODAL_ROOT: 'onboarding/explanation', + MIGRATED_USER_WELCOME_MODAL: 'onboarding/migrated-user-welcome', TRANSACTION_RECEIPT: { route: 'r/:reportID/transaction/:transactionID/receipt', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 4d360eaf0ac9..47090bd7075b 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -591,6 +591,10 @@ const SCREENS = { ROOT: 'Explanation_Modal_Root', }, + MIGRATED_USER_WELCOME_MODAL: { + ROOT: 'MigratedUserWelcomeModal_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/FeatureTrainingModal.tsx b/src/components/FeatureTrainingModal.tsx index 804fd69ee742..1ae76f72ccef 100644 --- a/src/components/FeatureTrainingModal.tsx +++ b/src/components/FeatureTrainingModal.tsx @@ -1,6 +1,7 @@ import type {VideoReadyForDisplayEvent} from 'expo-av'; import React, {useCallback, useEffect, useState} from 'react'; -import {View} from 'react-native'; +import {InteractionManager, View} from 'react-native'; +import type {StyleProp, ViewStyle} from 'react-native'; import {GestureHandlerRootView} from 'react-native-gesture-handler'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -39,6 +40,15 @@ type FeatureTrainingModalProps = { /** Animation to show when video is unavailable. Useful when app is offline */ animation?: DotLottieAnimation; + /** Style for the inner container of the animation */ + animationInnerContainerStyle?: StyleProp; + + /** Style for the outer container of the animation */ + animationOuterContainerStyle?: StyleProp; + + /** Additional styles for the animation */ + animationStyle?: StyleProp; + /** URL for the video */ videoURL: string; @@ -70,10 +80,25 @@ type FeatureTrainingModalProps = { /** Link to navigate to when user wants to learn more */ onHelp?: () => void; + + /** Children to render */ + children?: React.ReactNode; + + /** Styles for the content container */ + contentInnerContainerStyles?: StyleProp; + + /** Styles for the content outer container */ + contentOuterContainerStyles?: StyleProp; + + /** Styles for the modal inner container */ + modalInnerContainerStyle?: ViewStyle; }; function FeatureTrainingModal({ animation, + animationStyle, + animationInnerContainerStyle, + animationOuterContainerStyle, videoURL, videoAspectRatio: videoAspectRatioProp, title = '', @@ -85,11 +110,15 @@ function FeatureTrainingModal({ onClose = () => {}, helpText = '', onHelp = () => {}, + children, + contentInnerContainerStyles, + contentOuterContainerStyles, + modalInnerContainerStyle, }: FeatureTrainingModalProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {onboardingIsMediumOrLargerScreenWidth} = useResponsiveLayout(); - const [isModalVisible, setIsModalVisible] = useState(true); + const [isModalVisible, setIsModalVisible] = useState(false); const [willShowAgain, setWillShowAgain] = useState(true); const [videoStatus, setVideoStatus] = useState('video'); const [isVideoStatusLocked, setIsVideoStatusLocked] = useState(false); @@ -97,6 +126,10 @@ function FeatureTrainingModal({ const {shouldUseNarrowLayout} = useResponsiveLayout(); const {isOffline} = useNetwork(); + useEffect(() => { + InteractionManager.runAfterInteractions(() => setIsModalVisible(true)); + }, []); + useEffect(() => { if (isVideoStatusLocked) { return; @@ -133,10 +166,11 @@ function FeatureTrainingModal({ // for the video until it loads. Also, when // videoStatus === 'animation' it will // set the same aspect ratio as the video would. - {aspectRatio}, + animationInnerContainerStyle, + !!videoURL && {aspectRatio}, ]} > - {videoStatus === 'video' ? ( + {!!videoURL && videoStatus === 'video' ? ( ) : ( - + ); - }, [animation, videoURL, videoAspectRatio, videoStatus, shouldUseNarrowLayout, styles]); + }, [ + videoAspectRatio, + styles.w100, + styles.onboardingVideoPlayer, + styles.flex1, + styles.alignItemsCenter, + styles.justifyContentCenter, + styles.h100, + videoStatus, + videoURL, + animationStyle, + animation, + shouldUseNarrowLayout, + animationInnerContainerStyle, + ]); const toggleWillShowAgain = useCallback(() => setWillShowAgain((prevWillShowAgain) => !prevWillShowAgain), []); @@ -170,8 +218,10 @@ function FeatureTrainingModal({ User.dismissTrackTrainingModal(); } setIsModalVisible(false); - Navigation.goBack(); - onClose?.(); + InteractionManager.runAfterInteractions(() => { + Navigation.goBack(); + onClose?.(); + }); }, [onClose, willShowAgain]); const closeAndConfirmModal = useCallback(() => { @@ -199,16 +249,20 @@ function FeatureTrainingModal({ width: 'auto', } : {}), + ...modalInnerContainerStyle, }} > - {renderIllustration()} - + + {renderIllustration()} + + {!!title && !!description && ( - + {title} {description} {secondaryDescription.length > 0 && {secondaryDescription}} + {children} )} {shouldShowDismissModalOption && ( diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts index 0068fd30ed60..4379142619ff 100644 --- a/src/components/Icon/Illustrations.ts +++ b/src/components/Icon/Illustrations.ts @@ -90,6 +90,7 @@ import EmailAddress from '@assets/images/simple-illustrations/simple-illustratio import EmptyState from '@assets/images/simple-illustrations/simple-illustration__empty-state.svg'; import EnvelopeReceipt from '@assets/images/simple-illustrations/simple-illustration__envelopereceipt.svg'; import Filters from '@assets/images/simple-illustrations/simple-illustration__filters.svg'; +import Flash from '@assets/images/simple-illustrations/simple-illustration__flash.svg'; import FolderOpen from '@assets/images/simple-illustrations/simple-illustration__folder-open.svg'; import Gears from '@assets/images/simple-illustrations/simple-illustration__gears.svg'; import HandCard from '@assets/images/simple-illustrations/simple-illustration__handcard.svg'; @@ -105,6 +106,7 @@ import LockOpen from '@assets/images/simple-illustrations/simple-illustration__l import Luggage from '@assets/images/simple-illustrations/simple-illustration__luggage.svg'; import MagnifyingGlassMoney from '@assets/images/simple-illustrations/simple-illustration__magnifyingglass-money.svg'; import Mailbox from '@assets/images/simple-illustrations/simple-illustration__mailbox.svg'; +import ExpensifyMobileApp from '@assets/images/simple-illustrations/simple-illustration__mobileapp.svg'; import MoneyReceipts from '@assets/images/simple-illustrations/simple-illustration__money-receipts.svg'; import MoneyBadge from '@assets/images/simple-illustrations/simple-illustration__moneybadge.svg'; import MoneyIntoWallet from '@assets/images/simple-illustrations/simple-illustration__moneyintowallet.svg'; @@ -290,4 +292,6 @@ export { StripeCompanyCardDetailLarge, VisaCompanyCardDetailLarge, WellsFargoCompanyCardDetailLarge, + Flash, + ExpensifyMobileApp, }; diff --git a/src/components/MigratedUserWelcomeModal.tsx b/src/components/MigratedUserWelcomeModal.tsx new file mode 100644 index 000000000000..d097e3095298 --- /dev/null +++ b/src/components/MigratedUserWelcomeModal.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import {View} from 'react-native'; +import useLocalize from '@hooks/useLocalize'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as Welcome from '@libs/actions/Welcome'; +import convertToLTR from '@libs/convertToLTR'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import type {FeatureListItem} from './FeatureList'; +import FeatureTrainingModal from './FeatureTrainingModal'; +import Icon from './Icon'; +import * as Illustrations from './Icon/Illustrations'; +import LottieAnimations from './LottieAnimations'; +import RenderHTML from './RenderHTML'; + +const ExpensifyFeatures: FeatureListItem[] = [ + { + icon: Illustrations.ChatBubbles, + translationKey: 'migratedUserWelcomeModal.features.chat', + }, + { + icon: Illustrations.Flash, + translationKey: 'migratedUserWelcomeModal.features.scanReceipt', + }, + { + icon: Illustrations.ExpensifyMobileApp, + translationKey: 'migratedUserWelcomeModal.features.crossPlatform', + }, +]; + +function OnboardingWelcomeVideo() { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + + return ( + { + Welcome.dismissProductTraining(CONST.MIGRATED_USER_WELCOME_MODAL); + }} + animationStyle={[styles.emptyWorkspaceIllustrationStyle]} + animationInnerContainerStyle={[StyleUtils.getBackgroundColorStyle(LottieAnimations.WorkspacePlanet.backgroundColor), styles.cardSectionIllustration]} + animationOuterContainerStyle={styles.p0} + contentInnerContainerStyles={[styles.mb5, styles.gap2]} + contentOuterContainerStyles={!shouldUseNarrowLayout && [styles.mt8, styles.mh8]} + modalInnerContainerStyle={{...styles.pt0, ...(shouldUseNarrowLayout ? {} : styles.pb8)}} + > + + {ExpensifyFeatures.map(({translationKey, icon}) => ( + + + + ${convertToLTR(translate(translationKey))}`} /> + + + ))} + + + ); +} + +OnboardingWelcomeVideo.displayName = 'OnboardingWelcomeVideo'; +export default OnboardingWelcomeVideo; diff --git a/src/hooks/useOnboardingFlow.ts b/src/hooks/useOnboardingFlow.ts index 66ef088d0e4f..3a821dc44caf 100644 --- a/src/hooks/useOnboardingFlow.ts +++ b/src/hooks/useOnboardingFlow.ts @@ -2,7 +2,8 @@ import {useEffect} from 'react'; import {NativeModules} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import Navigation from '@libs/Navigation/Navigation'; -import {hasCompletedGuidedSetupFlowSelector, hasCompletedHybridAppOnboardingFlowSelector} from '@libs/onboardingSelectors'; +import {hasCompletedGuidedSetupFlowSelector, tryNewDotOnyxSelector} from '@libs/onboardingSelectors'; +import Permissions from '@libs/Permissions'; import * as OnboardingFlow from '@userActions/Welcome/OnboardingFlow'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -17,18 +18,26 @@ function useOnboardingFlowRouter() { const [isOnboardingCompleted, isOnboardingCompletedMetadata] = useOnyx(ONYXKEYS.NVP_ONBOARDING, { selector: hasCompletedGuidedSetupFlowSelector, }); - const [isHybridAppOnboardingCompleted, isHybridAppOnboardingCompletedMetadata] = useOnyx(ONYXKEYS.NVP_TRYNEWDOT, { - selector: hasCompletedHybridAppOnboardingFlowSelector, + const [tryNewDot, tryNewDotdMetadata] = useOnyx(ONYXKEYS.NVP_TRYNEWDOT, { + selector: tryNewDotOnyxSelector, }); + const {isHybridAppOnboardingCompleted, hasBeenAddedToNudgeMigration} = tryNewDot ?? {}; - const [isSingleNewDotEntry, isSingleNewDotEntryMetadata] = useOnyx(ONYXKEYS.IS_SINGLE_NEW_DOT_ENTRY); + const [dismissedProductTraining, dismissedProductTrainingMetadata] = useOnyx(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING); + const [isSingleNewDotEntry, isSingleNewDotEntryMetadata] = useOnyx(ONYXKEYS.IS_SINGLE_NEW_DOT_ENTRY); + const [allBetas, allBetasMetadata] = useOnyx(ONYXKEYS.BETAS); useEffect(() => { - if (isLoadingOnyxValue(isOnboardingCompletedMetadata)) { + if (isLoadingOnyxValue(isOnboardingCompletedMetadata, tryNewDotdMetadata, dismissedProductTrainingMetadata, allBetasMetadata)) { + return; + } + + if (NativeModules.HybridAppModule && isLoadingOnyxValue(isSingleNewDotEntryMetadata)) { return; } - if (NativeModules.HybridAppModule && isLoadingOnyxValue(isHybridAppOnboardingCompletedMetadata, isSingleNewDotEntryMetadata)) { + if (hasBeenAddedToNudgeMigration && !dismissedProductTraining?.migratedUserWelcomeModal && Permissions.shouldShowProductTrainingElements(allBetas)) { + Navigation.navigate(ROUTES.MIGRATED_USER_WELCOME_MODAL); return; } @@ -54,7 +63,20 @@ function useOnboardingFlowRouter() { if (!NativeModules.HybridAppModule && isOnboardingCompleted === false) { OnboardingFlow.startOnboardingFlow(); } - }, [isOnboardingCompleted, isHybridAppOnboardingCompleted, isOnboardingCompletedMetadata, isHybridAppOnboardingCompletedMetadata, isSingleNewDotEntryMetadata, isSingleNewDotEntry]); + }, [ + isOnboardingCompleted, + isHybridAppOnboardingCompleted, + isOnboardingCompletedMetadata, + tryNewDotdMetadata, + isSingleNewDotEntryMetadata, + isSingleNewDotEntry, + hasBeenAddedToNudgeMigration, + dismissedProductTrainingMetadata, + dismissedProductTraining?.migratedUserWelcomeModal, + dismissedProductTraining, + allBetasMetadata, + allBetas, + ]); return {isOnboardingCompleted, isHybridAppOnboardingCompleted}; } diff --git a/src/languages/en.ts b/src/languages/en.ts index 0cab2b81bf99..5c2cb34dc251 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -5439,6 +5439,16 @@ const translations = { takeATwoMinuteTour: 'Take a 2-minute tour', exploreExpensify: 'Explore everything Expensify has to offer', }, + migratedUserWelcomeModal: { + title: 'Travel and expense, at the speed of chat', + subtitle: 'New Expensify has the same great automation, but now with amazing collaboration:', + confirmText: "Let's go!", + features: { + chat: 'Chat directly on any expense, report, or workspace', + scanReceipt: 'Scan receipts and get paid back', + crossPlatform: 'Do everything from your phone or browser', + }, + }, }; export default translations satisfies TranslationDeepObject; diff --git a/src/languages/es.ts b/src/languages/es.ts index c56a8e02c6b9..bdd839e9192b 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -5959,6 +5959,16 @@ const translations = { takeATwoMinuteTour: 'Haz un tour de 2 minutos', exploreExpensify: 'Explora todo lo que Expensify tiene para ofrecer', }, + migratedUserWelcomeModal: { + title: 'Viajes y gastos, a la velocidad del chat', + subtitle: 'New Expensify tiene la misma excelente automatización, pero ahora con una colaboración increíble:', + confirmText: 'Vamos!', + features: { + chat: 'Chatea directamente en cualquier gasto, informe o espacio de trabajo', + scanReceipt: 'Escanea recibos y obtén reembolsos', + crossPlatform: 'Haz todo desde tu teléfono o navegador', + }, + }, }; export default translations satisfies TranslationDeepObject; diff --git a/src/libs/API/parameters/DismissProductTraining.ts b/src/libs/API/parameters/DismissProductTraining.ts new file mode 100644 index 000000000000..6a82ad995294 --- /dev/null +++ b/src/libs/API/parameters/DismissProductTraining.ts @@ -0,0 +1,5 @@ +type DismissProductTrainingParams = { + name: string; +}; + +export default DismissProductTrainingParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 6a510d074f98..837fc9189e56 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -352,3 +352,4 @@ export type {default as OpenPolicyPerDiemRatesPageParams} from './OpenPolicyPerD export type {default as TogglePlatformMuteParams} from './TogglePlatformMuteParams'; export type {default as ImportPerDiemRatesParams} from './ImportPerDiemRatesParams'; export type {default as ExportPerDiemCSVParams} from './ExportPerDiemCSVParams'; +export type {default as DismissProductTrainingParams} from './DismissProductTraining'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 892bad17928e..e7df0cc6a282 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -438,6 +438,7 @@ const WRITE_COMMANDS = { SELF_TOUR_VIEWED: 'SelfTourViewed', UPDATE_INVOICE_COMPANY_NAME: 'UpdateInvoiceCompanyName', UPDATE_INVOICE_COMPANY_WEBSITE: 'UpdateInvoiceCompanyWebsite', + DISMISS_PRODUCT_TRAINING: 'DismissProductTraining', } as const; type WriteCommand = ValueOf; @@ -887,6 +888,9 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SET_INVOICING_TRANSFER_BANK_ACCOUNT]: Parameters.SetInvoicingTransferBankAccountParams; [WRITE_COMMANDS.UPDATE_INVOICE_COMPANY_NAME]: Parameters.UpdateInvoiceCompanyNameParams; [WRITE_COMMANDS.UPDATE_INVOICE_COMPANY_WEBSITE]: Parameters.UpdateInvoiceCompanyWebsiteParams; + + // Dismis Product Training + [WRITE_COMMANDS.DISMISS_PRODUCT_TRAINING]: Parameters.DismissProductTrainingParams; }; const READ_COMMANDS = { diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index 7d4c18d03e90..be3139e0d65b 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -69,6 +69,7 @@ import ExplanationModalNavigator from './Navigators/ExplanationModalNavigator'; import FeatureTrainingModalNavigator from './Navigators/FeatureTrainingModalNavigator'; import FullScreenNavigator from './Navigators/FullScreenNavigator'; import LeftModalNavigator from './Navigators/LeftModalNavigator'; +import MigratedUserWelcomeModalNavigator from './Navigators/MigratedUserWelcomeModalNavigator'; import OnboardingModalNavigator from './Navigators/OnboardingModalNavigator'; import RightModalNavigator from './Navigators/RightModalNavigator'; import WelcomeVideoModalNavigator from './Navigators/WelcomeVideoModalNavigator'; @@ -522,6 +523,11 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie options={rootNavigatorOptions.basicModalNavigator} component={ExplanationModalNavigator} /> + (); + +function MigratedUserWelcomeModalNavigator() { + return ( + + + + + + + + ); +} + +MigratedUserWelcomeModalNavigator.displayName = 'MigratedUserWelcomeModalNavigator'; + +export default MigratedUserWelcomeModalNavigator; diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 5091773c92bb..c9dd7f17ad86 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -105,6 +105,16 @@ const config: LinkingOptions['config'] = { }, }, }, + + [NAVIGATORS.MIGRATED_USER_MODAL_NAVIGATOR]: { + screens: { + [SCREENS.MIGRATED_USER_WELCOME_MODAL.ROOT]: { + path: ROUTES.MIGRATED_USER_WELCOME_MODAL, + exact: true, + }, + }, + }, + [NAVIGATORS.ONBOARDING_MODAL_NAVIGATOR]: { // Don't set the initialRouteName, because when the user continues from the last visited onboarding page, // the onboarding purpose page will be briefly visible. diff --git a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts index 518d27da8feb..b0fba017b367 100644 --- a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts +++ b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts @@ -180,6 +180,7 @@ function getAdaptedState(state: PartialState 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 migratedUserModalNavigator = state.routes.find((route) => route.name === NAVIGATORS.MIGRATED_USER_MODAL_NAVIGATOR); const attachmentsScreen = state.routes.find((route) => route.name === SCREENS.ATTACHMENTS); const featureTrainingModalNavigator = state.routes.find((route) => route.name === NAVIGATORS.FEATURE_TRANING_MODAL_NAVIGATOR); @@ -224,7 +225,7 @@ function getAdaptedState(state: PartialState metainfo, }; } - if (lhpNavigator ?? onboardingModalNavigator ?? welcomeVideoModalNavigator ?? featureTrainingModalNavigator) { + if (lhpNavigator ?? onboardingModalNavigator ?? welcomeVideoModalNavigator ?? featureTrainingModalNavigator ?? migratedUserModalNavigator) { // Routes // - default bottom tab // - default central pane on desktop layout @@ -269,6 +270,10 @@ function getAdaptedState(state: PartialState routes.push(welcomeVideoModalNavigator); } + if (migratedUserModalNavigator) { + routes.push(migratedUserModalNavigator); + } + if (featureTrainingModalNavigator) { routes.push(featureTrainingModalNavigator); } diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 3674a24a907b..86948ea7f099 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1536,6 +1536,10 @@ type ExplanationModalNavigatorParamList = { [SCREENS.EXPLANATION_MODAL.ROOT]: undefined; }; +type MigratedUserModalNavigatorParamList = { + [SCREENS.MIGRATED_USER_WELCOME_MODAL.ROOT]: undefined; +}; + type BottomTabNavigatorParamList = { [SCREENS.HOME]: {policyID?: string}; [SCREENS.SEARCH.BOTTOM_TAB]: undefined; @@ -1609,6 +1613,7 @@ type AuthScreensParamList = CentralPaneScreensParamList & [NAVIGATORS.FEATURE_TRANING_MODAL_NAVIGATOR]: NavigatorScreenParams; [NAVIGATORS.WELCOME_VIDEO_MODAL_NAVIGATOR]: NavigatorScreenParams; [NAVIGATORS.EXPLANATION_MODAL_NAVIGATOR]: NavigatorScreenParams; + [NAVIGATORS.MIGRATED_USER_MODAL_NAVIGATOR]: NavigatorScreenParams; [SCREENS.DESKTOP_SIGN_IN_REDIRECT]: undefined; [SCREENS.TRANSACTION_RECEIPT]: { reportID: string; @@ -1766,4 +1771,5 @@ export type { RestrictedActionParamList, MissingPersonalDetailsParamList, DebugParamList, + MigratedUserModalNavigatorParamList, }; diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index 4a7bba3932a3..f77f992ede37 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -35,6 +35,11 @@ function canUsePerDiem(betas: OnyxEntry): boolean { return !!betas?.includes(CONST.BETAS.PER_DIEM) || canUseAllBetas(betas); } +// TEMPORARY BETA TO HIDE PRODUCT TRAINING TOOLTIP AND MIGRATE USER WELCOME MODAL +function shouldShowProductTrainingElements(betas: OnyxEntry): boolean { + return !!betas?.includes(CONST.BETAS.PRODUCT_TRAINING) || canUseAllBetas(betas); +} + /** * Link previews are temporarily disabled. */ @@ -50,4 +55,5 @@ export default { canUseCombinedTrackSubmit, canUseCategoryAndTagApprovers, canUsePerDiem, + shouldShowProductTrainingElements, }; diff --git a/src/libs/actions/Welcome/index.ts b/src/libs/actions/Welcome/index.ts index 607370ae3b5e..b306daf444ba 100644 --- a/src/libs/actions/Welcome/index.ts +++ b/src/libs/actions/Welcome/index.ts @@ -3,6 +3,7 @@ import type {OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; import {SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; +import DateUtils from '@libs/DateUtils'; import Log from '@libs/Log'; import type {OnboardingCompanySize} from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -204,9 +205,28 @@ function setSelfTourViewed(shouldUpdateOnyxDataOnlyLocally = false) { API.write(WRITE_COMMANDS.SELF_TOUR_VIEWED, null, {optimisticData}); } +function dismissProductTraining(elementName: string) { + const date = new Date(); + // const optimisticData = [ + // { + // onyxMethod: Onyx.METHOD.MERGE, + // key: ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, + // value: { + // [elementName]: DateUtils.getDBTime(date.valueOf()), + // }, + // }, + // ]; + // API.write(WRITE_COMMANDS.DISMISS_PRODUCT_TRAINING, {name: elementName}, {optimisticData}); + + Onyx.merge(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, { + [elementName]: DateUtils.getDBTime(date.valueOf()), + }); +} + export { onServerDataReady, isOnboardingFlowCompleted, + dismissProductTraining, setOnboardingCustomChoices, setOnboardingPurposeSelected, updateOnboardingLastVisitedPath, diff --git a/src/libs/onboardingSelectors.ts b/src/libs/onboardingSelectors.ts index 2487996335e2..91185e5c67bf 100644 --- a/src/libs/onboardingSelectors.ts +++ b/src/libs/onboardingSelectors.ts @@ -29,15 +29,16 @@ function hasCompletedGuidedSetupFlowSelector(onboarding: OnyxValue): boolean | undefined { - let completedHybridAppOnboarding = tryNewDotData?.classicRedirect?.completedHybridAppOnboarding; +function tryNewDotOnyxSelector(tryNewDotData: OnyxValue): {isHybridAppOnboardingCompleted: boolean | undefined; hasBeenAddedToNudgeMigration: boolean} { + let isHybridAppOnboardingCompleted = tryNewDotData?.classicRedirect?.completedHybridAppOnboarding; + const hasBeenAddedToNudgeMigration = !!tryNewDotData?.nudgeMigration?.timestamp; // Backend might return strings instead of booleans - if (typeof completedHybridAppOnboarding === 'string') { - completedHybridAppOnboarding = completedHybridAppOnboarding === 'true'; + if (typeof isHybridAppOnboardingCompleted === 'string') { + isHybridAppOnboardingCompleted = isHybridAppOnboardingCompleted === 'true'; } - return completedHybridAppOnboarding; + return {isHybridAppOnboardingCompleted, hasBeenAddedToNudgeMigration}; } /** @@ -55,4 +56,4 @@ function hasSeenTourSelector(onboarding: OnyxValue