Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
24 changes: 23 additions & 1 deletion src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<typeof CONST.MULTIFACTOR_AUTHENTICATION_NOTIFICATION_TYPE>) => `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;

/**
Expand All @@ -3700,7 +3722,7 @@ const SHARED_ROUTE_PARAMS: Partial<Record<Screen, string[]>> = {
[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;
Expand Down
8 changes: 8 additions & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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<typeof SCREENS>;
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<Text style={[styles.textAlignCenter, styles.textSupporting]}>
{start}
<TextLink onPress={goToSettings}>{link}</TextLink>
{end}
</Text>
);
}

NoEligibleMethodsDescription.displayName = 'NoEligibleMethodsDescription';

export default NoEligibleMethodsDescription;
38 changes: 38 additions & 0 deletions src/components/MultifactorAuthentication/PromptContent.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<View style={styles.flex1}>
<BlockingView
animation={animation}
animationStyles={styles.mfaBlockingViewAnimation}
animationWebStyle={styles.mfaBlockingViewAnimation}
title={translate(title)}
titleStyles={styles.mb2}
subtitle={translate(subtitle)}
subtitleStyle={styles.textSupporting}
containerStyle={styles.ph5}
testID="MultifactorAuthenticationPromptContent"
/>
</View>
);
}

MultifactorAuthenticationPromptContent.displayName = 'MultifactorAuthenticationPromptContent';

export default MultifactorAuthenticationPromptContent;
Original file line number Diff line number Diff line change
@@ -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<string, TranslationPaths>;

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 (
<ConfirmModal
danger
title={title}
onConfirm={onConfirm}
onCancel={onCancel}
isVisible={isVisible}
prompt={description}
confirmText={confirmButtonText}
cancelText={cancelButtonText}
shouldShowCancelButton
/>
);
}

MultifactorAuthenticationTriggerCancelConfirmModal.displayName = 'MultifactorAuthenticationTriggerCancelConfirmModal';

export default MultifactorAuthenticationTriggerCancelConfirmModal;
Original file line number Diff line number Diff line change
@@ -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<MultifactorAuthenticationValidateCodeResendButtonHandle>;
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<ValidateCodeCountdownHandle>(null);

const handleCountdownFinish = useCallback(() => {
setIsCountdownRunning(false);
}, []);

useImperativeHandle(ref, () => ({
resetCountdown: () => {
countdownRef.current?.resetCountdown();
setIsCountdownRunning(true);
},
}));

return (
<View style={styles.alignItemsStart}>
{isCountdownRunning && !isOffline ? (
<View style={[styles.mt5, styles.flexRow, styles.renderHTML]}>
<ValidateCodeCountdown
ref={countdownRef}
onCountdownFinish={handleCountdownFinish}
/>
</View>
) : (
<PressableWithFeedback
style={styles.mt5}
onPress={onResendValidationCode}
disabled={shouldDisableResendCode}
hoverDimmingValue={1}
pressDimmingValue={0.2}
role={CONST.ROLE.BUTTON}
accessibilityLabel={translate(resendButtonText)}
>
<Text style={[StyleUtils.getDisabledLinkStyles(shouldDisableResendCode)]}>
{hasError ? translate('validateCodeForm.requestNewCodeAfterErrorOccurred') : translate(resendButtonText)}
</Text>
</PressableWithFeedback>
)}
</View>
);
}

MultifactorAuthenticationValidateCodeResendButton.displayName = 'MultifactorAuthenticationValidateCodeResendButton';

export default MultifactorAuthenticationValidateCodeResendButton;

export type {MultifactorAuthenticationValidateCodeResendButtonHandle};
27 changes: 27 additions & 0 deletions src/components/TestToolMenu.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 (
<>
<Text
Expand Down Expand Up @@ -83,6 +99,17 @@ function TestToolMenu() {
onPress={clearLHNCache}
/>
</TestToolRow>

{/* Allows you to test the Biometrics flow */}
<TestToolRow title={biometricsTitle}>
<View style={[styles.flexRow, styles.gap2]}>
<Button
small
text={translate('multifactorAuthentication.biometricsTest.test')}
onPress={() => navigateToBiometricsTestPage()}
/>
</View>
</TestToolRow>
</>
)}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import type {AutoCompleteVariant, MagicCodeInputHandle} from '@components/MagicC
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
import Text from '@components/Text';
import ValidateCodeCountdown from '@components/ValidateCodeCountdown';
import type {ValidateCodeCountdownHandle} from '@components/ValidateCodeCountdown/types';
import {WideRHPContext} from '@components/WideRHPContextProvider';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
Expand All @@ -20,8 +22,6 @@ import useThemeStyles from '@hooks/useThemeStyles';
import {isMobileSafari} from '@libs/Browser';
import {getLatestErrorField, getLatestErrorMessage} from '@libs/ErrorUtils';
import {isValidValidateCode} from '@libs/ValidationUtils';
import ValidateCodeCountdown from '@pages/signin/ValidateCodeCountdown';
import type {ValidateCodeCountdownHandle} from '@pages/signin/ValidateCodeCountdown/types';
import {clearValidateCodeActionError} from '@userActions/User';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
Expand Down
10 changes: 10 additions & 0 deletions src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
MergeTransactionNavigatorParamList,
MissingPersonalDetailsParamList,
MoneyRequestNavigatorParamList,
MultifactorAuthenticationParamList,
NewChatNavigatorParamList,
NewReportWorkspaceSelectionNavigatorParamList,
NewTaskNavigatorParamList,
Expand Down Expand Up @@ -984,6 +985,14 @@ const WorkspacesDomainModalStackNavigator = createModalStackNavigator<Workspaces
[SCREENS.WORKSPACES_DOMAIN_ACCESS_RESTRICTED]: () => require<ReactComponentModule>('../../../../pages/domain/DomainAccessRestrictedPage').default,
});

const MultifactorAuthenticationStackNavigator = createModalStackNavigator<MultifactorAuthenticationParamList>({
[SCREENS.MULTIFACTOR_AUTHENTICATION.MAGIC_CODE]: () => require<ReactComponentModule>('../../../../pages/MultifactorAuthentication/ValidateCodePage').default,
[SCREENS.MULTIFACTOR_AUTHENTICATION.BIOMETRICS_TEST]: () => require<ReactComponentModule>('../../../../pages/MultifactorAuthentication/BiometricsTestPage').default,
[SCREENS.MULTIFACTOR_AUTHENTICATION.NOTIFICATION]: () => require<ReactComponentModule>('../../../../pages/MultifactorAuthentication/NotificationPage').default,
[SCREENS.MULTIFACTOR_AUTHENTICATION.PROMPT]: () => require<ReactComponentModule>('../../../../pages/MultifactorAuthentication/PromptPage').default,
[SCREENS.MULTIFACTOR_AUTHENTICATION.NOT_FOUND]: () => require<ReactComponentModule>('../../../../pages/ErrorPage/NotFoundPage').default,
});

export {
AddPersonalBankAccountModalStackNavigator,
AddUnreportedExpenseModalStackNavigator,
Expand Down Expand Up @@ -1033,4 +1042,5 @@ export {
WorkspaceConfirmationModalStackNavigator,
WorkspaceDuplicateModalStackNavigator,
WorkspacesDomainModalStackNavigator,
MultifactorAuthenticationStackNavigator,
};
Loading
Loading