diff --git a/src/CONST/index.ts b/src/CONST/index.ts index e7f7cff62dd52..78daa0c31e506 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -5469,6 +5469,10 @@ const CONST = { DISABLED: 'DISABLED', DISABLE: 'DISABLE', }, + MULTIFACTOR_AUTHENTICATION_NOTIFICATION_TYPE: { + SUCCESS: 'success', + FAILURE: 'failure', + }, MERGE_ACCOUNT_RESULTS: { SUCCESS: 'success', ERR_2FA: 'err_2fa', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 1799b9078b2ba..87990bbe91f86 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -51,6 +51,11 @@ const PUBLIC_SCREENS_ROUTES = { // Exported for identifying a url as a verify-account route, associated with a page extending the VerifyAccountPageBase component const VERIFY_ACCOUNT = 'verify-account'; +const MULTIFACTOR_AUTHENTICATION_PROTECTED_ROUTES = { + FACTOR: 'multifactor-authentication/factor', + PROMPT: 'multifactor-authentication/prompt', +} as const; + const ROUTES = { ...PUBLIC_SCREENS_ROUTES, // This route renders the list of reports. @@ -3685,6 +3690,23 @@ const ROUTES = { route: 'domain/:domainAccountID/admins/:accountID/reset-domain', getRoute: (domainAccountID: number, accountID: number) => `domain/${domainAccountID}/admins/${accountID}/reset-domain` as const, }, + + MULTIFACTOR_AUTHENTICATION_MAGIC_CODE: `${MULTIFACTOR_AUTHENTICATION_PROTECTED_ROUTES.FACTOR}/magic-code`, + MULTIFACTOR_AUTHENTICATION_BIOMETRICS_TEST: 'multifactor-authentication/scenario/biometrics-test', + + // The exact notification & prompt type will be added as a part of Multifactor Authentication config in another PR, + // for now a string is accepted to avoid blocking this PR. + MULTIFACTOR_AUTHENTICATION_NOTIFICATION: { + route: 'multifactor-authentication/notification/:notificationType', + getRoute: (notificationType: ValueOf) => `multifactor-authentication/notification/${notificationType}` as const, + }, + + MULTIFACTOR_AUTHENTICATION_PROMPT: { + route: `${MULTIFACTOR_AUTHENTICATION_PROTECTED_ROUTES.PROMPT}/:promptType`, + getRoute: (promptType: string) => `${MULTIFACTOR_AUTHENTICATION_PROTECTED_ROUTES.PROMPT}/${promptType}` as const, + }, + + MULTIFACTOR_AUTHENTICATION_NOT_FOUND: 'multifactor-authentication/not-found', } as const; /** @@ -3700,7 +3722,7 @@ const SHARED_ROUTE_PARAMS: Partial> = { [SCREENS.WORKSPACE.INITIAL]: ['backTo'], } as const; -export {PUBLIC_SCREENS_ROUTES, SHARED_ROUTE_PARAMS, VERIFY_ACCOUNT}; +export {PUBLIC_SCREENS_ROUTES, SHARED_ROUTE_PARAMS, VERIFY_ACCOUNT, MULTIFACTOR_AUTHENTICATION_PROTECTED_ROUTES}; export default ROUTES; type ReportAttachmentsRoute = typeof ROUTES.REPORT_ATTACHMENTS.route; diff --git a/src/SCREENS.ts b/src/SCREENS.ts index c6f861e0666db..9744c56eac725 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -276,6 +276,7 @@ const SCREENS = { REPORT_CARD_ACTIVATE: 'Report_Card_Activate', DOMAIN: 'Domain', EXPENSE_REPORT: 'ExpenseReport', + MULTIFACTOR_AUTHENTICATION: 'MultifactorAuthentication', }, REPORT_CARD_ACTIVATE: 'Report_Card_Activate_Root', PUBLIC_CONSOLE_DEBUG: 'Console_Debug', @@ -883,6 +884,13 @@ const SCREENS = { MEMBER_DETAILS: 'Member_Details', RESET_DOMAIN: 'Domain_Reset', }, + MULTIFACTOR_AUTHENTICATION: { + MAGIC_CODE: 'Multifactor_Authentication_Magic_Code', + BIOMETRICS_TEST: 'Multifactor_Authentication_Biometrics_Test', + NOTIFICATION: 'Multifactor_Authentication_Notification', + PROMPT: 'Multifactor_Authentication_Prompt', + NOT_FOUND: 'Multifactor_Authentication_Not_Found', + }, } as const; type Screen = DeepValueOf; diff --git a/src/components/MultifactorAuthentication/NoEligibleMethodsDescription.tsx b/src/components/MultifactorAuthentication/NoEligibleMethodsDescription.tsx new file mode 100644 index 0000000000000..b1dcab2c7dcad --- /dev/null +++ b/src/components/MultifactorAuthentication/NoEligibleMethodsDescription.tsx @@ -0,0 +1,34 @@ +import Text from '@components/Text'; +import TextLink from '@components/TextLink'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import goToSettings from '@libs/goToSettings'; + +const baseTranslationPath = 'multifactorAuthentication.pleaseEnableInSystemSettings' as const; + +const translationPaths = { + start: `${baseTranslationPath}.start`, + link: `${baseTranslationPath}.link`, + end: `${baseTranslationPath}.end`, +} as const; + +function NoEligibleMethodsDescription() { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const start = translate(translationPaths.start); + const link = translate(translationPaths.link); + const end = translate(translationPaths.end); + + return ( + + {start} + {link} + {end} + + ); +} + +NoEligibleMethodsDescription.displayName = 'NoEligibleMethodsDescription'; + +export default NoEligibleMethodsDescription; diff --git a/src/components/MultifactorAuthentication/PromptContent.tsx b/src/components/MultifactorAuthentication/PromptContent.tsx new file mode 100644 index 0000000000000..2d91ff96d1d4a --- /dev/null +++ b/src/components/MultifactorAuthentication/PromptContent.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import {View} from 'react-native'; +import BlockingView from '@components/BlockingViews/BlockingView'; +import type DotLottieAnimation from '@components/LottieAnimations/types'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type {TranslationPaths} from '@src/languages/types'; + +type MultifactorAuthenticationPromptContentProps = { + animation: DotLottieAnimation; + title: TranslationPaths; + subtitle: TranslationPaths; +}; + +function MultifactorAuthenticationPromptContent({title, subtitle, animation}: MultifactorAuthenticationPromptContentProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + return ( + + + + ); +} + +MultifactorAuthenticationPromptContent.displayName = 'MultifactorAuthenticationPromptContent'; + +export default MultifactorAuthenticationPromptContent; diff --git a/src/components/MultifactorAuthentication/TriggerCancelConfirmModal.tsx b/src/components/MultifactorAuthentication/TriggerCancelConfirmModal.tsx new file mode 100644 index 0000000000000..819b385074972 --- /dev/null +++ b/src/components/MultifactorAuthentication/TriggerCancelConfirmModal.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import ConfirmModal from '@components/ConfirmModal'; +import useLocalize from '@hooks/useLocalize'; +import type {TranslationPaths} from '@src/languages/types'; + +type MultifactorAuthenticationTriggerCancelConfirmModalProps = { + isVisible: boolean; + onConfirm: () => void; + onCancel: () => void; +}; + +// TODO: this config will be part of the scenario configuration, the current implementation is for testing purposes (https://github.com/Expensify/App/issues/79373) +const mockedConfig = { + title: 'common.areYouSure', + description: 'multifactorAuthentication.biometricsTest.areYouSureToReject', + confirmButtonText: 'multifactorAuthentication.biometricsTest.rejectAuthentication', + cancelButtonText: 'common.cancel', +} as const satisfies Record; + +function MultifactorAuthenticationTriggerCancelConfirmModal({isVisible, onConfirm, onCancel}: MultifactorAuthenticationTriggerCancelConfirmModalProps) { + const {translate} = useLocalize(); + + const title = translate(mockedConfig.title); + const description = translate(mockedConfig.description); + const confirmButtonText = translate(mockedConfig.confirmButtonText); + const cancelButtonText = translate(mockedConfig.cancelButtonText); + + return ( + + ); +} + +MultifactorAuthenticationTriggerCancelConfirmModal.displayName = 'MultifactorAuthenticationTriggerCancelConfirmModal'; + +export default MultifactorAuthenticationTriggerCancelConfirmModal; diff --git a/src/components/MultifactorAuthentication/ValidateCodeResendButton.tsx b/src/components/MultifactorAuthentication/ValidateCodeResendButton.tsx new file mode 100644 index 0000000000000..e129d4461e186 --- /dev/null +++ b/src/components/MultifactorAuthentication/ValidateCodeResendButton.tsx @@ -0,0 +1,84 @@ +import React, {useCallback, useImperativeHandle, useRef, useState} from 'react'; +import {View} from 'react-native'; +import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; +import Text from '@components/Text'; +import ValidateCodeCountdown from '@components/ValidateCodeCountdown'; +import type {ValidateCodeCountdownHandle} from '@components/ValidateCodeCountdown/types'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; + +type MultifactorAuthenticationValidateCodeResendButtonHandle = { + resetCountdown: () => void; +}; + +type MultifactorAuthenticationValidateCodeResendButtonProps = { + ref?: React.Ref; + shouldDisableResendCode: boolean; + hasError: boolean; + resendButtonText: TranslationPaths; + onResendValidationCode: () => void; +}; + +function MultifactorAuthenticationValidateCodeResendButton({ + ref, + shouldDisableResendCode, + hasError, + resendButtonText, + onResendValidationCode, +}: MultifactorAuthenticationValidateCodeResendButtonProps) { + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const {translate} = useLocalize(); + const {isOffline} = useNetwork(); + + const [isCountdownRunning, setIsCountdownRunning] = useState(true); + const countdownRef = useRef(null); + + const handleCountdownFinish = useCallback(() => { + setIsCountdownRunning(false); + }, []); + + useImperativeHandle(ref, () => ({ + resetCountdown: () => { + countdownRef.current?.resetCountdown(); + setIsCountdownRunning(true); + }, + })); + + return ( + + {isCountdownRunning && !isOffline ? ( + + + + ) : ( + + + {hasError ? translate('validateCodeForm.requestNewCodeAfterErrorOccurred') : translate(resendButtonText)} + + + )} + + ); +} + +MultifactorAuthenticationValidateCodeResendButton.displayName = 'MultifactorAuthenticationValidateCodeResendButton'; + +export default MultifactorAuthenticationValidateCodeResendButton; + +export type {MultifactorAuthenticationValidateCodeResendButtonHandle}; diff --git a/src/components/TestToolMenu.tsx b/src/components/TestToolMenu.tsx index d31450a022500..f4f83bfab0008 100644 --- a/src/components/TestToolMenu.tsx +++ b/src/components/TestToolMenu.tsx @@ -1,15 +1,20 @@ import React from 'react'; +import {View} from 'react-native'; import useIsAuthenticated from '@hooks/useIsAuthenticated'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import {useSidebarOrderedReports} from '@hooks/useSidebarOrderedReports'; +import useSingleExecution from '@hooks/useSingleExecution'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWaitForNavigation from '@hooks/useWaitForNavigation'; import {isUsingStagingApi} from '@libs/ApiUtils'; +import Navigation from '@libs/Navigation/Navigation'; import {setShouldFailAllRequests, setShouldForceOffline, setShouldSimulatePoorConnection} from '@userActions/Network'; import {expireSessionWithDelay, invalidateAuthToken, invalidateCredentials} from '@userActions/Session'; import {setIsDebugModeEnabled, setShouldUseStagingServer} from '@userActions/User'; import CONFIG from '@src/CONFIG'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import Button from './Button'; import SoftKillTestToolRow from './SoftKillTestToolRow'; import Switch from './Switch'; @@ -26,9 +31,20 @@ function TestToolMenu() { const {translate} = useLocalize(); const {clearLHNCache} = useSidebarOrderedReports(); + const {singleExecution} = useSingleExecution(); + const waitForNavigate = useWaitForNavigation(); + const navigateToBiometricsTestPage = singleExecution( + waitForNavigate(() => { + Navigation.navigate(ROUTES.MULTIFACTOR_AUTHENTICATION_BIOMETRICS_TEST); + }), + ); + // Check if the user is authenticated to show options that require authentication const isAuthenticated = useIsAuthenticated(); + // Temporary hardcoded false, expected behavior: status fetched from the MultifactorAuthenticationContext + const biometricsTitle = translate('multifactorAuthentication.biometricsTest.troubleshootBiometricsStatus', {registered: false}); + return ( <> + + {/* Allows you to test the Biometrics flow */} + + +