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')}
+
+
+
+
+ );
+}
+
+RequireTwoFactorAuthenticationPage.displayName = 'RequireTwoFactorAuthenticationPage';
+
+export default RequireTwoFactorAuthenticationPage;
diff --git a/src/pages/settings/Security/TwoFactorAuth/SuccessPage.tsx b/src/pages/settings/Security/TwoFactorAuth/SuccessPage.tsx
index 05a3ddf9b17f..93b46b79cd59 100644
--- a/src/pages/settings/Security/TwoFactorAuth/SuccessPage.tsx
+++ b/src/pages/settings/Security/TwoFactorAuth/SuccessPage.tsx
@@ -1,9 +1,10 @@
-import React from 'react';
+import React, {useCallback, useEffect} from 'react';
import ConfirmationPage from '@components/ConfirmationPage';
import LottieAnimations from '@components/LottieAnimations';
import useEnvironment from '@hooks/useEnvironment';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
+import Navigation from '@libs/Navigation/Navigation';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
import type {TwoFactorAuthNavigatorParamList} from '@libs/Navigation/types';
import {openLink} from '@userActions/Link';
@@ -20,6 +21,25 @@ function SuccessPage({route}: SuccessPageProps) {
const {environmentURL} = useEnvironment();
const styles = useThemeStyles();
+ const goBack = useCallback(() => {
+ if (route.params?.backTo === ROUTES.REQUIRE_TWO_FACTOR_AUTH) {
+ Navigation.dismissModal();
+ return;
+ }
+ quitAndNavigateBack(route.params?.backTo ?? ROUTES.SETTINGS_2FA_ROOT.getRoute());
+ }, [route.params?.backTo]);
+
+ useEffect(() => {
+ return () => {
+ // When the 2FA RHP is closed, we want to remove the 2FA required page fromt the navigation stack too.
+ if (route.params?.backTo !== ROUTES.REQUIRE_TWO_FACTOR_AUTH) {
+ return;
+ }
+ Navigation.popRootToTop();
+ };
+ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
+ }, []);
+
return (
{
- quitAndNavigateBack(route.params?.backTo ?? ROUTES.SETTINGS_2FA_ROOT.getRoute());
-
+ goBack();
if (route.params?.forwardTo) {
openLink(route.params.forwardTo, environmentURL);
}
diff --git a/src/styles/index.ts b/src/styles/index.ts
index 565d354ccfe9..701a145f4b8d 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -5286,6 +5286,11 @@ const styles = (theme: ThemeColors) =>
width: variables.updateTextViewContainerWidth,
},
+ twoFARequiredContainer: {
+ maxWidth: 350,
+ margin: 'auto',
+ },
+
widthAuto: {
width: 'auto',
},