From 1865b8a94743103d7789dea222f0c32c2c6218ea Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Mon, 10 Mar 2025 13:20:00 +0800 Subject: [PATCH 1/7] show a fullscreen 2fa required instead of modal --- .../simple-illustration__encryption.svg | 80 +++++++++++++++++++ src/Expensify.tsx | 21 ----- src/ROUTES.ts | 1 + src/SCREENS.ts | 1 + src/components/Icon/Illustrations.ts | 2 + src/languages/en.ts | 4 +- src/languages/es.ts | 5 +- .../Navigation/AppNavigator/AuthScreens.tsx | 19 ++++- src/libs/Navigation/NavigationRoot.tsx | 13 +-- src/libs/Navigation/linkingConfig/config.ts | 1 + src/libs/Navigation/types.ts | 1 + .../RequireTwoFactorAuthenticationPage.tsx | 54 +++++++++++++ .../Security/TwoFactorAuth/SuccessPage.tsx | 15 +++- src/styles/utils/spacing.ts | 4 + 14 files changed, 188 insertions(+), 33 deletions(-) create mode 100644 assets/images/simple-illustrations/simple-illustration__encryption.svg create mode 100644 src/pages/RequireTwoFactorAuthenticationPage.tsx 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 85e8f6c6af88..3ec5be1e613c 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 UpdateAppModal from './components/UpdateAppModal'; @@ -40,7 +39,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'; @@ -88,7 +86,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}); @@ -97,13 +94,6 @@ function Expensify() { const [focusModeNotification] = useOnyx(ONYXKEYS.FOCUS_MODE_NOTIFICATION, {initWithStoredValues: false}); const [lastVisitedPath] = useOnyx(ONYXKEYS.LAST_VISITED_PATH); - useEffect(() => { - if (!account?.needsTwoFactorAuthSetup || account.requiresTwoFactorAuth) { - return; - } - setShouldShowRequire2FAModal(true); - }, [account?.needsTwoFactorAuthSetup, account?.requiresTwoFactorAuth]); - const [initialUrl, setInitialUrl] = useState(null); useEffect(() => { @@ -282,16 +272,6 @@ function Expensify() { /> ) : null} {focusModeNotification ? : null} - {shouldShowRequire2FAModal ? ( - { - setShouldShowRequire2FAModal(false); - Navigation.navigate(ROUTES.SETTINGS_2FA_ROOT.getRoute(ROUTES.HOME)); - }} - isVisible - description={translate('twoFactorAuth.twoFactorAuthIsRequiredForAdminsDescription')} - /> - ) : null} )} @@ -302,7 +282,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 79c41dd9b975..81eaa57c8df4 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -119,6 +119,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 7e09c0277fe4..ded8693fc4e4 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -208,6 +208,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 737d588127a9..91083198ebc5 100644 --- a/src/components/Icon/Illustrations.ts +++ b/src/components/Icon/Illustrations.ts @@ -92,6 +92,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'; @@ -154,6 +155,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 780e6d037398..0dadcc9f0a4f 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1398,7 +1398,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 523df0aa1a28..087e85911c97 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1397,8 +1397,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 7b52887f3905..ca10e562d844 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -3,7 +3,7 @@ import {findFocusedRoute, useNavigation} from '@react-navigation/native'; import React, {memo, useEffect, useMemo, useRef} from 'react'; import {NativeModules} from 'react-native'; 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'; @@ -36,6 +36,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'; @@ -219,8 +220,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 = account?.needsTwoFactorAuthSetup && !account.requiresTwoFactorAuth; const navigation = useNavigation(); useEffect(() => { @@ -243,6 +246,13 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie }; }, [theme]); + 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; @@ -597,6 +607,13 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie }} /> )} + {shouldShowRequire2FAPage && ( + + )} 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, }); @@ -102,6 +100,7 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady, sh selector: wasInvitedToNewDotSelector, }); const [hasNonPersonalPolicy] = useOnyx(ONYXKEYS.HAS_NON_PERSONAL_POLICY); + const shouldShowRequire2FAPage = account?.needsTwoFactorAuthSetup && !account.requiresTwoFactorAuth; const previousAuthenticated = usePrevious(authenticated); @@ -110,12 +109,16 @@ function NavigationRoot({authenticated, lastVisitedPath, initialUrl, onReady, sh return; } + 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 (!NativeModules.HybridAppModule && !hasNonPersonalPolicy && !isOnboardingCompleted && !wasInvitedToNewDot && authenticated && !isTransitioning && !shouldShowRequire2FAModal) { + if (!NativeModules.HybridAppModule && !hasNonPersonalPolicy && !isOnboardingCompleted && !wasInvitedToNewDot && authenticated && !isTransitioning && !shouldShowRequire2FAPage) { return getAdaptedStateFromPath(getOnboardingInitialPath(isPrivateDomain), linkingConfig.config); } diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 848c4ed9cdcd..1846c4da2fae 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 45d9119f0a7e..93825e64d343 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1807,6 +1807,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..225753f5a4a6 --- /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')} + +