Skip to content

Commit

Permalink
Merge pull request #44059 from rushatgabhane/2fa
Browse files Browse the repository at this point in the history
  • Loading branch information
francoisl authored Jul 16, 2024
2 parents d900cf6 + dcddaa2 commit 5b35a06
Show file tree
Hide file tree
Showing 14 changed files with 244 additions and 14 deletions.
5 changes: 5 additions & 0 deletions .storybook/webpack.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ const webpackConfig = ({config}: {config: Configuration}) => {
}),
);

config.module.rules?.push({
test: /\.lottie$/,
type: 'asset/resource',
});

return config;
};

Expand Down
24 changes: 23 additions & 1 deletion src/Expensify.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import React, {useCallback, useEffect, useLayoutEffect, useMemo, useRef, useStat
import type {NativeEventSubscription} from 'react-native';
import {AppState, Linking, 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 ConfirmModal from './components/ConfirmModal';
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';
Expand Down Expand Up @@ -37,6 +38,7 @@ 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 type {ScreenShareRequest, Session} from './types/onyx';

Onyx.registerLogger(({level, message}) => {
Expand Down Expand Up @@ -101,6 +103,16 @@ function Expensify({
const [isSplashHidden, setIsSplashHidden] = useState(false);
const [hasAttemptedToOpenPublicRoom, setAttemptedToOpenPublicRoom] = useState(false);
const {translate} = useLocalize();
const [account] = useOnyx(ONYXKEYS.ACCOUNT);
const [shouldShowRequire2FAModal, setShouldShowRequire2FAModal] = useState(false);

useEffect(() => {
if (!account?.needsTwoFactorAuthSetup || account.requiresTwoFactorAuth) {
return;
}
setShouldShowRequire2FAModal(true);
}, [account?.needsTwoFactorAuthSetup, account?.requiresTwoFactorAuth]);

const [initialUrl, setInitialUrl] = useState<string | null>(null);

useEffect(() => {
Expand Down Expand Up @@ -253,6 +265,16 @@ function Expensify({
/>
) : null}
{focusModeNotification ? <FocusModeNotification /> : null}
{shouldShowRequire2FAModal ? (
<RequireTwoFactorAuthenticationModal
onSubmit={() => {
setShouldShowRequire2FAModal(false);
Navigation.navigate(ROUTES.SETTINGS_2FA.getRoute(ROUTES.HOME));
}}
isVisible
description={translate('twoFactorAuth.twoFactorAuthIsRequiredForAdminsDescription')}
/>
) : null}
</>
)}

Expand Down
3 changes: 2 additions & 1 deletion src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,8 @@ const ROUTES = {
},
SETTINGS_2FA: {
route: 'settings/security/two-factor-auth',
getRoute: (backTo?: string) => getUrlWithBackToParam('settings/security/two-factor-auth', backTo),
getRoute: (backTo?: string, forwardTo?: string) =>
getUrlWithBackToParam(forwardTo ? `settings/security/two-factor-auth?forwardTo=${encodeURIComponent(forwardTo)}` : 'settings/security/two-factor-auth', backTo),
},
SETTINGS_STATUS: 'settings/profile/status',

Expand Down
26 changes: 25 additions & 1 deletion src/components/ConnectToXeroButton/index.native.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import React, {useRef, useState} from 'react';
import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import {useOnyx, withOnyx} from 'react-native-onyx';
import {WebView} from 'react-native-webview';
import AccountingConnectionConfirmationModal from '@components/AccountingConnectionConfirmationModal';
import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView';
import Button from '@components/Button';
import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import Modal from '@components/Modal';
import RequireTwoFactorAuthenticationModal from '@components/RequireTwoFactorAuthenticationModal';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
import {removePolicyConnection} from '@libs/actions/connections';
import {getXeroSetupLink} from '@libs/actions/connections/ConnectToXero';
import getUAForWebView from '@libs/getUAForWebView';
import Navigation from '@libs/Navigation/Navigation';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {Session} from '@src/types/onyx';
import type {ConnectToXeroButtonProps} from './types';

Expand All @@ -33,13 +36,22 @@ function ConnectToXeroButton({policyID, session, shouldDisconnectIntegrationBefo
const authToken = session?.authToken ?? null;
const {isOffline} = useNetwork();

const [account] = useOnyx(ONYXKEYS.ACCOUNT);
const is2FAEnabled = account?.requiresTwoFactorAuth ?? false;

const renderLoading = () => <FullScreenLoadingIndicator />;
const [isDisconnectModalOpen, setIsDisconnectModalOpen] = useState(false);
const [isRequire2FAModalOpen, setIsRequire2FAModalOpen] = useState(false);

return (
<>
<Button
onPress={() => {
if (!is2FAEnabled) {
setIsRequire2FAModalOpen(true);
return;
}

if (shouldDisconnectIntegrationBeforeConnecting && integrationToDisconnect) {
setIsDisconnectModalOpen(true);
return;
Expand All @@ -62,6 +74,18 @@ function ConnectToXeroButton({policyID, session, shouldDisconnectIntegrationBefo
onCancel={() => setIsDisconnectModalOpen(false)}
/>
)}
{isRequire2FAModalOpen && (
<RequireTwoFactorAuthenticationModal
onSubmit={() => {
setIsRequire2FAModalOpen(false);
Navigation.dismissModal();
Navigation.navigate(ROUTES.SETTINGS_2FA.getRoute(ROUTES.POLICY_ACCOUNTING.getRoute(policyID), getXeroSetupLink(policyID)));
}}
onCancel={() => setIsRequire2FAModalOpen(false)}
isVisible
description={translate('twoFactorAuth.twoFactorAuthIsRequiredDescription')}
/>
)}
<Modal
onClose={() => setWebViewOpen(false)}
fullscreen
Expand Down
26 changes: 26 additions & 0 deletions src/components/ConnectToXeroButton/index.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import React, {useState} from 'react';
import {useOnyx} from 'react-native-onyx';
import AccountingConnectionConfirmationModal from '@components/AccountingConnectionConfirmationModal';
import Button from '@components/Button';
import RequireTwoFactorAuthenticationModal from '@components/RequireTwoFactorAuthenticationModal';
import useEnvironment from '@hooks/useEnvironment';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
import {removePolicyConnection} from '@libs/actions/connections';
import {getXeroSetupLink} from '@libs/actions/connections/ConnectToXero';
import Navigation from '@libs/Navigation/Navigation';
import * as Link from '@userActions/Link';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {ConnectToXeroButtonProps} from './types';

function ConnectToXeroButton({policyID, shouldDisconnectIntegrationBeforeConnecting, integrationToDisconnect}: ConnectToXeroButtonProps) {
Expand All @@ -17,12 +22,21 @@ function ConnectToXeroButton({policyID, shouldDisconnectIntegrationBeforeConnect
const {environmentURL} = useEnvironment();
const {isOffline} = useNetwork();

const [account] = useOnyx(ONYXKEYS.ACCOUNT);
const is2FAEnabled = account?.requiresTwoFactorAuth;

const [isDisconnectModalOpen, setIsDisconnectModalOpen] = useState(false);
const [isRequire2FAModalOpen, setIsRequire2FAModalOpen] = useState(false);

return (
<>
<Button
onPress={() => {
if (!is2FAEnabled) {
setIsRequire2FAModalOpen(true);
return;
}

if (shouldDisconnectIntegrationBeforeConnecting && integrationToDisconnect) {
setIsDisconnectModalOpen(true);
return;
Expand All @@ -45,6 +59,18 @@ function ConnectToXeroButton({policyID, shouldDisconnectIntegrationBeforeConnect
onCancel={() => setIsDisconnectModalOpen(false)}
/>
)}
{isRequire2FAModalOpen && (
<RequireTwoFactorAuthenticationModal
onSubmit={() => {
setIsRequire2FAModalOpen(false);
Navigation.dismissModal();
Navigation.navigate(ROUTES.SETTINGS_2FA.getRoute(ROUTES.POLICY_ACCOUNTING.getRoute(policyID), getXeroSetupLink(policyID)));
}}
onCancel={() => setIsRequire2FAModalOpen(false)}
isVisible
description={translate('twoFactorAuth.twoFactorAuthIsRequiredDescription')}
/>
)}
</>
);
}
Expand Down
91 changes: 91 additions & 0 deletions src/components/RequireTwoFactorAuthenticationModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React from 'react';
import {View} from 'react-native';
import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
import Button from './Button';
import Lottie from './Lottie';
import LottieAnimations from './LottieAnimations';
import Modal from './Modal';
import SafeAreaConsumer from './SafeAreaConsumer';
import Text from './Text';

type RequireTwoFactorAuthenticationModalProps = {
/** A callback to call when the form has been submitted */
onSubmit: () => void;

/** A callback to call when the form has been closed */
onCancel?: () => void;

/** Modal visibility */
isVisible: boolean;

/** Describe what is showing */
description: string;

/**
* Whether the modal should enable the new focus manager.
* We are attempting to migrate to a new refocus manager, adding this property for gradual migration.
* */
shouldEnableNewFocusManagement?: boolean;
};

function RequireTwoFactorAuthenticationModal({onCancel = () => {}, description, isVisible, onSubmit, shouldEnableNewFocusManagement}: RequireTwoFactorAuthenticationModalProps) {
const {shouldUseNarrowLayout} = useResponsiveLayout();
const styles = useThemeStyles();
const {translate} = useLocalize();
const StyleUtils = useStyleUtils();

return (
<SafeAreaConsumer>
{({safeAreaPaddingBottomStyle}) => (
<Modal
onClose={onCancel}
isVisible={isVisible}
type={shouldUseNarrowLayout ? CONST.MODAL.MODAL_TYPE.BOTTOM_DOCKED : CONST.MODAL.MODAL_TYPE.CONFIRM}
innerContainerStyle={{...styles.pb5, ...styles.pt3, ...styles.boxShadowNone}}
shouldEnableNewFocusManagement={shouldEnableNewFocusManagement}
>
<View style={safeAreaPaddingBottomStyle}>
<View
style={[
styles.mh3,
styles.br3,
styles.cardSectionIllustration,
styles.alignItemsCenter,
StyleUtils.getBackgroundColorStyle(LottieAnimations.Safe.backgroundColor),
]}
>
<Lottie
source={LottieAnimations.Safe}
style={styles.h100}
webStyle={styles.h100}
autoPlay
loop
/>
</View>
<View style={[styles.mt5, styles.mh5]}>
<View style={[styles.gap2, styles.mb10]}>
<Text style={[styles.textHeadlineH1]}>{translate('twoFactorAuth.pleaseEnableTwoFactorAuth')}</Text>
<Text style={styles.textSupporting}>{description}</Text>
</View>
<Button
large
success
pressOnEnter
onPress={onSubmit}
text={translate('twoFactorAuth.enableTwoFactorAuth')}
/>
</View>
</View>
</Modal>
)}
</SafeAreaConsumer>
);
}

RequireTwoFactorAuthenticationModal.displayName = 'RequireTwoFactorAuthenticationModal';

export default RequireTwoFactorAuthenticationModal;
5 changes: 5 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1069,6 +1069,11 @@ export default {
enabled: 'Two-factor authentication is now enabled!',
congrats: 'Congrats, now you’ve got that extra security.',
copy: 'Copy',
disable: 'Disable',
enableTwoFactorAuth: 'Enable two-factor authentication',
pleaseEnableTwoFactorAuth: 'Please enable two-factor authentication.',
twoFactorAuthIsRequiredDescription: 'Two-factor authentication is required for connecting to Xero. Please enable two-factor authentication to continue.',
twoFactorAuthIsRequiredForAdminsDescription: 'Two-factor authentication is required for Xero workspace admins. Please enable two-factor authentication to continue.',
},
recoveryCodeForm: {
error: {
Expand Down
6 changes: 6 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1077,6 +1077,12 @@ export default {
enabled: '¡La autenticación de dos factores está ahora habilitada!',
congrats: 'Felicidades, ahora tienes esa seguridad adicional.',
copy: 'Copiar',
disable: 'Deshabilitar',
enableTwoFactorAuth: 'Activar la autenticación de dos factores',
pleaseEnableTwoFactorAuth: 'Activa la autenticación de dos factores.',
twoFactorAuthIsRequiredDescription: 'La autenticación de dos factores es necesaria para conectarse a Xero. Activa la autenticación de dos factores para continuar.',
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.',
},
recoveryCodeForm: {
error: {
Expand Down
4 changes: 2 additions & 2 deletions src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,6 @@ const WRITE_COMMANDS = {
UNLINK_LOGIN: 'UnlinkLogin',
ENABLE_TWO_FACTOR_AUTH: 'EnableTwoFactorAuth',
DISABLE_TWO_FACTOR_AUTH: 'DisableTwoFactorAuth',
TWO_FACTOR_AUTH_VALIDATE: 'TwoFactorAuth_Validate',
ADD_COMMENT: 'AddComment',
ADD_ATTACHMENT: 'AddAttachment',
ADD_TEXT_AND_ATTACHMENT: 'AddTextAndAttachment',
Expand Down Expand Up @@ -379,7 +378,6 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.UNLINK_LOGIN]: Parameters.UnlinkLoginParams;
[WRITE_COMMANDS.ENABLE_TWO_FACTOR_AUTH]: null;
[WRITE_COMMANDS.DISABLE_TWO_FACTOR_AUTH]: null;
[WRITE_COMMANDS.TWO_FACTOR_AUTH_VALIDATE]: Parameters.ValidateTwoFactorAuthParams;
[WRITE_COMMANDS.ADD_COMMENT]: Parameters.AddCommentOrAttachementParams;
[WRITE_COMMANDS.ADD_ATTACHMENT]: Parameters.AddCommentOrAttachementParams;
[WRITE_COMMANDS.ADD_TEXT_AND_ATTACHMENT]: Parameters.AddCommentOrAttachementParams;
Expand Down Expand Up @@ -750,6 +748,7 @@ const SIDE_EFFECT_REQUEST_COMMANDS = {
ADD_PAYMENT_CARD_GBR: 'AddPaymentCardGBP',
REVEAL_EXPENSIFY_CARD_DETAILS: 'RevealExpensifyCardDetails',
SWITCH_TO_OLD_DOT: 'SwitchToOldDot',
TWO_FACTOR_AUTH_VALIDATE: 'TwoFactorAuth_Validate',
} as const;

type SideEffectRequestCommand = ValueOf<typeof SIDE_EFFECT_REQUEST_COMMANDS>;
Expand All @@ -765,6 +764,7 @@ type SideEffectRequestCommandParameters = {
[SIDE_EFFECT_REQUEST_COMMANDS.GENERATE_SPOTNANA_TOKEN]: Parameters.GenerateSpotnanaTokenParams;
[SIDE_EFFECT_REQUEST_COMMANDS.ADD_PAYMENT_CARD_GBR]: Parameters.AddPaymentCardParams;
[SIDE_EFFECT_REQUEST_COMMANDS.ACCEPT_SPOTNANA_TERMS]: null;
[SIDE_EFFECT_REQUEST_COMMANDS.TWO_FACTOR_AUTH_VALIDATE]: Parameters.ValidateTwoFactorAuthParams;
};

type ApiRequestCommandParameters = WriteCommandParameters & ReadCommandParameters & SideEffectRequestCommandParameters;
Expand Down
Loading

0 comments on commit 5b35a06

Please sign in to comment.