diff --git a/assets/images/simple-illustrations/simple-illustration__encryption.svg b/assets/images/simple-illustrations/simple-illustration__encryption.svg new file mode 100644 index 000000000000..a440855383ca --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__encryption.svg @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Expensify.tsx b/src/Expensify.tsx index 0db7270dc8ab..135423808885 100644 --- a/src/Expensify.tsx +++ b/src/Expensify.tsx @@ -9,7 +9,6 @@ import DeeplinkWrapper from './components/DeeplinkWrapper'; import EmojiPicker from './components/EmojiPicker/EmojiPicker'; import FocusModeNotification from './components/FocusModeNotification'; import GrowlNotification from './components/GrowlNotification'; -import RequireTwoFactorAuthenticationModal from './components/RequireTwoFactorAuthenticationModal'; import AppleAuthWrapper from './components/SignInButtons/AppleAuthWrapper'; import SplashScreenHider from './components/SplashScreenHider'; import TestToolsModal from './components/TestToolsModal'; @@ -43,7 +42,6 @@ import ONYXKEYS from './ONYXKEYS'; import PopoverReportActionContextMenu from './pages/home/report/ContextMenu/PopoverReportActionContextMenu'; import * as ReportActionContextMenu from './pages/home/report/ContextMenu/ReportActionContextMenu'; import type {Route} from './ROUTES'; -import ROUTES from './ROUTES'; import SplashScreenStateContext from './SplashScreenStateContext'; import type {ScreenShareRequest} from './types/onyx'; @@ -91,7 +89,6 @@ function Expensify() { const [session] = useOnyx(ONYXKEYS.SESSION); const [lastRoute] = useOnyx(ONYXKEYS.LAST_ROUTE); const [userMetadata] = useOnyx(ONYXKEYS.USER_METADATA); - const [shouldShowRequire2FAModal, setShouldShowRequire2FAModal] = useState(false); const [isCheckingPublicRoom] = useOnyx(ONYXKEYS.IS_CHECKING_PUBLIC_ROOM, {initWithStoredValues: false}); const [updateAvailable] = useOnyx(ONYXKEYS.UPDATE_AVAILABLE, {initWithStoredValues: false}); const [updateRequired] = useOnyx(ONYXKEYS.UPDATE_REQUIRED, {initWithStoredValues: false}); @@ -102,13 +99,6 @@ function Expensify() { useDebugShortcut(); - useEffect(() => { - if (!account?.needsTwoFactorAuthSetup || account.requiresTwoFactorAuth) { - return; - } - setShouldShowRequire2FAModal(true); - }, [account?.needsTwoFactorAuthSetup, account?.requiresTwoFactorAuth]); - const [initialUrl, setInitialUrl] = useState(null); useEffect(() => { @@ -287,16 +277,6 @@ function Expensify() { /> ) : null} {focusModeNotification ? : null} - {shouldShowRequire2FAModal ? ( - { - setShouldShowRequire2FAModal(false); - Navigation.navigate(ROUTES.SETTINGS_2FA_ROOT.getRoute(ROUTES.HOME)); - }} - isVisible - description={translate('twoFactorAuth.twoFactorAuthIsRequiredForAdminsDescription')} - /> - ) : null} )} @@ -307,7 +287,6 @@ function Expensify() { authenticated={isAuthenticated} lastVisitedPath={lastVisitedPath as Route} initialUrl={initialUrl} - shouldShowRequire2FAModal={shouldShowRequire2FAModal} /> )} {shouldHideSplash && } diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 299903505320..e722caeed905 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -115,6 +115,7 @@ const ROUTES = { ENABLE_PAYMENTS: 'enable-payments', WALLET_STATEMENT_WITH_DATE: 'statements/:yearMonth', SIGN_IN_MODAL: 'sign-in-modal', + REQUIRE_TWO_FACTOR_AUTH: '2fa-required', BANK_ACCOUNT: 'bank-account', BANK_ACCOUNT_NEW: 'bank-account/new', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 06051422bb13..526bcbaec712 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -212,6 +212,7 @@ const SCREENS = { DESKTOP_SIGN_IN_REDIRECT: 'DesktopSignInRedirect', SAML_SIGN_IN: 'SAMLSignIn', WORKSPACE_JOIN_USER: 'WorkspaceJoinUser', + REQUIRE_TWO_FACTOR_AUTH: 'RequireTwoFactorAuth', MONEY_REQUEST: { CREATE: 'Money_Request_Create', diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts index 60961a746a8c..7c2a7b55c5b1 100644 --- a/src/components/Icon/Illustrations.ts +++ b/src/components/Icon/Illustrations.ts @@ -93,6 +93,7 @@ import CreditCardEyes from '@assets/images/simple-illustrations/simple-illustrat import CreditCardsNewGreen from '@assets/images/simple-illustrations/simple-illustration__creditcards--green.svg'; import EmailAddress from '@assets/images/simple-illustrations/simple-illustration__email-address.svg'; import EmptyState from '@assets/images/simple-illustrations/simple-illustration__empty-state.svg'; +import Encryption from '@assets/images/simple-illustrations/simple-illustration__encryption.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'; @@ -155,6 +156,7 @@ import TurtleInShell from '@assets/images/turtle-in-shell.svg'; export { Abracadabra, + Encryption, BankArrowPink, BankMouseGreen, BankUserGreen, diff --git a/src/languages/en.ts b/src/languages/en.ts index 5f44cde8afb5..a87cc19fad14 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1393,7 +1393,9 @@ const translations = { enableTwoFactorAuth: 'Enable two-factor authentication', pleaseEnableTwoFactorAuth: 'Please enable two-factor authentication.', twoFactorAuthIsRequiredDescription: 'For security purposes, Xero requires two-factor authentication to connect the integration.', - twoFactorAuthIsRequiredForAdminsDescription: 'Two-factor authentication is required for Xero workspace admins. Please enable two-factor authentication to continue.', + twoFactorAuthIsRequiredForAdminsHeader: 'Two-factor authentication required', + twoFactorAuthIsRequiredForAdminsTitle: 'You need to enable two-factor authentication', + twoFactorAuthIsRequiredForAdminsDescription: 'The Xero accounting connection requires the use of two-factor authentication. To continue using Expensify, please enable it.', twoFactorAuthCannotDisable: 'Cannot disable 2FA', twoFactorAuthRequired: 'Two-factor authentication (2FA) is required for your Xero connection and cannot be disabled.', }, diff --git a/src/languages/es.ts b/src/languages/es.ts index 33b62a5eb8d4..462dc0b563eb 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1392,8 +1392,9 @@ const translations = { enableTwoFactorAuth: 'Activar la autenticación de dos factores', pleaseEnableTwoFactorAuth: 'Activa la autenticación de dos factores.', twoFactorAuthIsRequiredDescription: 'Por razones de seguridad, Xero requiere la autenticación de dos factores para conectar la integración.', - twoFactorAuthIsRequiredForAdminsDescription: - 'La autenticación de dos factores es necesaria para los administradores del área de trabajo de Xero. Activa la autenticación de dos factores para continuar.', + twoFactorAuthIsRequiredForAdminsHeader: 'Se requiere autenticación de dos factores', + twoFactorAuthIsRequiredForAdminsTitle: 'Debes habilitar la autenticación de dos factores', + twoFactorAuthIsRequiredForAdminsDescription: 'La conexión contable con Xero requiere el uso de autenticación de dos factores. Para seguir usando Expensify, por favor, habilítala.', twoFactorAuthCannotDisable: 'No se puede desactivar la autenticación de dos factores (2FA)', twoFactorAuthRequired: 'La autenticación de dos factores (2FA) es obligatoria para tu conexión a Xero y no se puede desactivar.', }, diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index 25ac2cc17943..abeae7e42701 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -1,8 +1,8 @@ import type {RouteProp} from '@react-navigation/native'; import {findFocusedRoute, useNavigation} from '@react-navigation/native'; -import React, {memo, useEffect, useMemo, useRef} from 'react'; +import React, {memo, useEffect, useMemo, useRef, useState} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; -import Onyx, {withOnyx} from 'react-native-onyx'; +import Onyx, {useOnyx, withOnyx} from 'react-native-onyx'; import ActiveGuidesEventListener from '@components/ActiveGuidesEventListener'; import ActiveWorkspaceContextProvider from '@components/ActiveWorkspaceProvider'; import ComposeProviders from '@components/ComposeProviders'; @@ -34,6 +34,7 @@ import PusherConnectionManager from '@libs/PusherConnectionManager'; import * as SessionUtils from '@libs/SessionUtils'; import ConnectionCompletePage from '@pages/ConnectionCompletePage'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; +import RequireTwoFactorAuthenticationPage from '@pages/RequireTwoFactorAuthenticationPage'; import DesktopSignInRedirectPage from '@pages/signin/DesktopSignInRedirectPage'; import * as App from '@userActions/App'; import * as Download from '@userActions/Download'; @@ -216,8 +217,10 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const {toggleSearch} = useSearchRouterContext(); + const [account] = useOnyx(ONYXKEYS.ACCOUNT); const modal = useRef({}); const {isOnboardingCompleted} = useOnboardingFlowRouter(); + const [shouldShowRequire2FAPage, setShouldShowRequire2FAPage] = useState(!!account?.needsTwoFactorAuthSetup && !account.requiresTwoFactorAuth); const navigation = useNavigation(); useEffect(() => { @@ -240,6 +243,20 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie }; }, [theme]); + useEffect(() => { + if (!account?.needsTwoFactorAuthSetup || !!account.requiresTwoFactorAuth || shouldShowRequire2FAPage) { + return; + } + setShouldShowRequire2FAPage(true); + }, [account?.needsTwoFactorAuthSetup, account?.requiresTwoFactorAuth, shouldShowRequire2FAPage]); + + useEffect(() => { + if (!shouldShowRequire2FAPage) { + return; + } + Navigation.navigate(ROUTES.REQUIRE_TWO_FACTOR_AUTH); + }, [shouldShowRequire2FAPage]); + useEffect(() => { const shortcutsOverviewShortcutConfig = CONST.KEYBOARD_SHORTCUTS.SHORTCUTS; const searchShortcutConfig = CONST.KEYBOARD_SHORTCUTS.SEARCH; @@ -537,7 +554,20 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie name={NAVIGATORS.RIGHT_MODAL_NAVIGATOR} options={rootNavigatorScreenOptions.rightModalNavigator} component={RightModalNavigator} - listeners={modalScreenListenersWithCancelSearch} + listeners={{ + ...modalScreenListenersWithCancelSearch, + beforeRemove: () => { + modalScreenListenersWithCancelSearch.beforeRemove(); + + // When a 2FA RHP page is closed, if the 2FA require page is visible and the user has now enabled the 2FA, then remove the 2FA require page from the navigator. + const routeParams = navigation.getState()?.routes?.at(-1)?.params; + const screen = routeParams && 'screen' in routeParams ? routeParams.screen : ''; + if (!shouldShowRequire2FAPage || !account?.requiresTwoFactorAuth || screen !== SCREENS.RIGHT_MODAL.TWO_FACTOR_AUTH) { + return; + } + setShouldShowRequire2FAPage(false); + }, + }} /> )} + {shouldShowRequire2FAPage && ( + + )} { navigationRef.current?.dispatch((state) => { @@ -616,6 +621,7 @@ export default { setNavigationActionToMicrotaskQueue, navigateToReportWithPolicyCheck, popToTop, + popRootToTop, removeScreenFromNavigationState, removeScreenByKey, getReportRouteByID, diff --git a/src/libs/Navigation/NavigationRoot.tsx b/src/libs/Navigation/NavigationRoot.tsx index 3168a3aaab23..fc7a5ef68243 100644 --- a/src/libs/Navigation/NavigationRoot.tsx +++ b/src/libs/Navigation/NavigationRoot.tsx @@ -43,9 +43,6 @@ type NavigationRootProps = { /** Fired when react-navigation is ready */ onReady: () => void; - - /** Flag to indicate if the require 2FA modal should be shown to the user */ - shouldShowRequire2FAModal: boolean; }; /** @@ -83,7 +80,7 @@ function parseAndLogRoute(state: NavigationState) { } } -function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady, shouldShowRequire2FAModal}: NavigationRootProps) { +function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady}: NavigationRootProps) { const firstRenderRef = useRef(true); const themePreference = useThemePreference(); const theme = useTheme(); @@ -95,6 +92,7 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady, sh const [user] = useOnyx(ONYXKEYS.USER); const isPrivateDomain = Session.isUserOnPrivateDomain(); + const [account] = useOnyx(ONYXKEYS.ACCOUNT); const [isOnboardingCompleted = true] = useOnyx(ONYXKEYS.NVP_ONBOARDING, { selector: hasCompletedGuidedSetupFlowSelector, }); @@ -110,12 +108,17 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady, sh return; } + const shouldShowRequire2FAPage = !!account?.needsTwoFactorAuthSetup && !account.requiresTwoFactorAuth; + if (shouldShowRequire2FAPage) { + return getAdaptedStateFromPath(ROUTES.REQUIRE_TWO_FACTOR_AUTH, linkingConfig.config); + } + const path = initialUrl ? getPathFromURL(initialUrl) : null; const isTransitioning = path?.includes(ROUTES.TRANSITION_BETWEEN_APPS); // If the user haven't completed the flow, we want to always redirect them to the onboarding flow. // We also make sure that the user is authenticated, isn't part of a group workspace, isn't in the transition flow & wasn't invited to NewDot. - if (!CONFIG.IS_HYBRID_APP && !hasNonPersonalPolicy && !isOnboardingCompleted && !wasInvitedToNewDot && authenticated && !isTransitioning && !shouldShowRequire2FAModal) { + if (!CONFIG.IS_HYBRID_APP && !hasNonPersonalPolicy && !isOnboardingCompleted && !wasInvitedToNewDot && authenticated && !isTransitioning) { return getAdaptedStateFromPath(getOnboardingInitialPath(isPrivateDomain), linkingConfig.config); } diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 467d22eedcb4..f56b71e6519e 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -29,6 +29,7 @@ const config: LinkingOptions['config'] = { [SCREENS.REPORT_AVATAR]: ROUTES.REPORT_AVATAR.route, [SCREENS.TRANSACTION_RECEIPT]: ROUTES.TRANSACTION_RECEIPT.route, [SCREENS.WORKSPACE_JOIN_USER]: ROUTES.WORKSPACE_JOIN_USER.route, + [SCREENS.REQUIRE_TWO_FACTOR_AUTH]: ROUTES.REQUIRE_TWO_FACTOR_AUTH, [SCREENS.NOT_FOUND]: '*', [NAVIGATORS.LEFT_MODAL_NAVIGATOR]: { diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index f3c3d89203bb..0601187a7178 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1833,6 +1833,7 @@ type AuthScreensParamList = SharedScreensParamList & { policyID?: string; }; [SCREENS.NOT_FOUND]: undefined; + [SCREENS.REQUIRE_TWO_FACTOR_AUTH]: undefined; [NAVIGATORS.REPORTS_SPLIT_NAVIGATOR]: NavigatorScreenParams & {policyID?: string}; [NAVIGATORS.SETTINGS_SPLIT_NAVIGATOR]: NavigatorScreenParams & {policyID?: string}; [NAVIGATORS.WORKSPACE_SPLIT_NAVIGATOR]: NavigatorScreenParams; diff --git a/src/pages/RequireTwoFactorAuthenticationPage.tsx b/src/pages/RequireTwoFactorAuthenticationPage.tsx new file mode 100644 index 000000000000..350f97702a9a --- /dev/null +++ b/src/pages/RequireTwoFactorAuthenticationPage.tsx @@ -0,0 +1,54 @@ +import React from 'react'; +import {View} from 'react-native'; +import Button from '@components/Button'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import Icon from '@components/Icon'; +import {Encryption} from '@components/Icon/Illustrations'; +import ScreenWrapper from '@components/ScreenWrapper'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import variables from '@styles/variables'; +import ROUTES from '@src/ROUTES'; + +function RequireTwoFactorAuthenticationPage() { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + return ( + + + + + + + + + {translate('twoFactorAuth.twoFactorAuthIsRequiredForAdminsTitle')} + {translate('twoFactorAuth.twoFactorAuthIsRequiredForAdminsDescription')} + +