diff --git a/app/actions/security/index.ts b/app/actions/security/index.ts new file mode 100644 index 00000000000..c62bf08e450 --- /dev/null +++ b/app/actions/security/index.ts @@ -0,0 +1,20 @@ +/* eslint-disable import/prefer-default-export */ +import type { Action } from 'redux'; + +export enum ActionType { + SET_ALLOW_LOGIN_WITH_REMEMBER_ME = 'SET_ALLOW_LOGIN_WITH_REMEMBER_ME', +} + +export interface AllowLoginWithRememberMeUpdated + extends Action { + enabled: boolean; +} + +export type Action = AllowLoginWithRememberMeUpdated; + +export const setAllowLoginWithRememberMe = ( + enabled: boolean, +): AllowLoginWithRememberMeUpdated => ({ + type: ActionType.SET_ALLOW_LOGIN_WITH_REMEMBER_ME, + enabled, +}); diff --git a/app/actions/security/state.ts b/app/actions/security/state.ts new file mode 100644 index 00000000000..bf5abaf43e3 --- /dev/null +++ b/app/actions/security/state.ts @@ -0,0 +1,3 @@ +export interface SecuritySettingsState { + allowLoginWithRememberMe: boolean; +} diff --git a/app/components/Nav/App/index.js b/app/components/Nav/App/index.js index 238353df91d..8746a67fe83 100644 --- a/app/components/Nav/App/index.js +++ b/app/components/Nav/App/index.js @@ -54,6 +54,7 @@ import ModalConfirmation from '../../../component-library/components/Modals/Moda import Toast, { ToastContext, } from '../../../component-library/components/Toast'; +import { TurnOffRememberMeModal } from '../../../components/UI/TurnOffRememberMeModal'; const Stack = createStackNavigator(); /** @@ -351,6 +352,10 @@ const App = ({ userLoggedIn }) => { component={ModalConfirmation} /> + ); diff --git a/app/components/UI/LoginOptionsSwitch/LoginOptionsSwitch.test.tsx b/app/components/UI/LoginOptionsSwitch/LoginOptionsSwitch.test.tsx new file mode 100644 index 00000000000..e47368b24c0 --- /dev/null +++ b/app/components/UI/LoginOptionsSwitch/LoginOptionsSwitch.test.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import LoginOptionsSwitch from './LoginOptionsSwitch'; +import { BIOMETRY_TYPE } from 'react-native-keychain'; +import { Provider } from 'react-redux'; +import configureMockStore from 'redux-mock-store'; +describe('LoginWithBiometricsSwitch', () => { + const mockStore = configureMockStore(); + // eslint-disable-next-line @typescript-eslint/no-empty-function + const handleUpdate = (_biometricsEnabled: boolean) => {}; + it('should render correctly', () => { + const store = mockStore({}); + const wrapper = shallow( + + + , + ); + expect(wrapper).toMatchSnapshot(); + }); + + it('should return empty object when shouldRenderBiometricOption is undefined and allowLoginWithRememberMe is false in settings', () => { + const store = mockStore({}); + const wrapper = shallow( + + + , + ); + expect(wrapper).toMatchObject({}); + }); +}); diff --git a/app/components/UI/LoginOptionsSwitch/LoginOptionsSwitch.tsx b/app/components/UI/LoginOptionsSwitch/LoginOptionsSwitch.tsx new file mode 100644 index 00000000000..b7b0c218048 --- /dev/null +++ b/app/components/UI/LoginOptionsSwitch/LoginOptionsSwitch.tsx @@ -0,0 +1,96 @@ +import React, { useCallback, useState } from 'react'; +import { View, Switch, Text } from 'react-native'; +import { mockTheme, useAppThemeFromContext } from '../../../util/theme'; +import { strings } from '../../../../locales/i18n'; +import { BIOMETRY_TYPE } from 'react-native-keychain'; +import { createStyles } from './styles'; +import { + LOGIN_WITH_BIOMETRICS_SWITCH, + LOGIN_WITH_REMEMBER_ME_SWITCH, +} from '../../../constants/test-ids'; +import { useSelector } from 'react-redux'; + +interface Props { + shouldRenderBiometricOption: BIOMETRY_TYPE | null; + biometryChoiceState: boolean; + onUpdateBiometryChoice: (biometryEnabled: boolean) => void; + onUpdateRememberMe: (rememberMeEnabled: boolean) => void; +} + +/** + * View that renders the toggle for login options + * The highest priority login option is biometrics and will always get rendered over other options IF it is enabled. + * If the user has enabled login with remember me in settings and has turned off biometrics then remember me will be the option + * If both of these features are disabled then no options will be rendered + */ +const LoginOptionsSwitch = ({ + shouldRenderBiometricOption, + biometryChoiceState, + onUpdateBiometryChoice, + onUpdateRememberMe, +}: Props) => { + const { colors } = useAppThemeFromContext() || mockTheme; + const styles = createStyles(colors); + const allowLoginWithRememberMe = useSelector( + (state: any) => state.security.allowLoginWithRememberMe, + ); + const [rememberMeEnabled, setRememberMeEnabled] = useState(false); + const onBiometryValueChanged = useCallback( + async (newBiometryChoice: boolean) => { + onUpdateBiometryChoice(newBiometryChoice); + }, + [onUpdateBiometryChoice], + ); + + const onRememberMeValueChanged = useCallback(async () => { + onUpdateRememberMe(!rememberMeEnabled); + setRememberMeEnabled(!rememberMeEnabled); + }, [onUpdateRememberMe, rememberMeEnabled]); + + // should only render remember me option if biometrics are disabled and rememberOptionMeEnabled is enabled in security settings + // if both are disabled then this component returns null + if (shouldRenderBiometricOption !== null) { + return ( + + + {strings( + `biometrics.enable_${shouldRenderBiometricOption.toLowerCase()}`, + )} + + + + ); + } else if (shouldRenderBiometricOption === null && allowLoginWithRememberMe) { + return ( + + + {strings(`choose_password.remember_me`)} + + + + ); + } + return null; +}; + +export default React.memo(LoginOptionsSwitch); diff --git a/app/components/UI/LoginOptionsSwitch/__snapshots__/LoginOptionsSwitch.test.tsx.snap b/app/components/UI/LoginOptionsSwitch/__snapshots__/LoginOptionsSwitch.test.tsx.snap new file mode 100644 index 00000000000..d9e20aa46c2 --- /dev/null +++ b/app/components/UI/LoginOptionsSwitch/__snapshots__/LoginOptionsSwitch.test.tsx.snap @@ -0,0 +1,10 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LoginWithBiometricsSwitch should render correctly 1`] = ` + +`; diff --git a/app/components/UI/LoginOptionsSwitch/index.ts b/app/components/UI/LoginOptionsSwitch/index.ts new file mode 100644 index 00000000000..1de1c5d6af2 --- /dev/null +++ b/app/components/UI/LoginOptionsSwitch/index.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line import/prefer-default-export +export { default as LoginOptionsSwitch } from './LoginOptionsSwitch'; diff --git a/app/components/UI/LoginOptionsSwitch/styles.ts b/app/components/UI/LoginOptionsSwitch/styles.ts new file mode 100644 index 00000000000..a8c8c05fb3b --- /dev/null +++ b/app/components/UI/LoginOptionsSwitch/styles.ts @@ -0,0 +1,27 @@ +/* eslint-disable import/prefer-default-export */ +import { fontStyles } from '../../../styles/common'; +import { StyleSheet } from 'react-native'; + +export const createStyles = (colors: any) => + StyleSheet.create({ + screen: { + flex: 1, + backgroundColor: colors.background.default, + }, + container: { + position: 'relative', + marginTop: 20, + marginBottom: 30, + }, + label: { + flex: 1, + fontSize: 16, + color: colors.text.default, + ...fontStyles.normal, + }, + switch: { + position: 'absolute', + top: 0, + right: 0, + }, + }); diff --git a/app/components/UI/SecurityOptionToggle/SecurityOptionToggle.tsx b/app/components/UI/SecurityOptionToggle/SecurityOptionToggle.tsx new file mode 100644 index 00000000000..dde3483d93b --- /dev/null +++ b/app/components/UI/SecurityOptionToggle/SecurityOptionToggle.tsx @@ -0,0 +1,59 @@ +import React, { useCallback } from 'react'; +import { Switch, Text, View } from 'react-native'; +import { mockTheme, useAppThemeFromContext } from '../../../util/theme'; +import { createStyles } from './styles'; +import { colors as importedColors } from '../../../styles/common'; + +interface SecurityOptionsToggleProps { + title: string; + description?: string; + value: boolean; + onOptionUpdated: (enabled: boolean) => void; + testId?: string; + disabled?: boolean; +} + +/** + * View that renders the toggle for security options + * This component assumes that the parent will manage the state of the toggle. This is because most of the state is global. + */ +const SecurityOptionToggle = ({ + title, + description, + value, + testId, + onOptionUpdated, + disabled, +}: SecurityOptionsToggleProps) => { + const { colors } = useAppThemeFromContext() || mockTheme; + const styles = createStyles(colors); + + const handleOnValueChange = useCallback( + (newValue: boolean) => { + onOptionUpdated(newValue); + }, + [onOptionUpdated], + ); + return ( + + {title} + {description ? {description} : null} + + handleOnValueChange(newValue)} + trackColor={{ + true: colors.primary.default, + false: colors.border.muted, + }} + thumbColor={importedColors.white} + style={styles.switch} + ios_backgroundColor={colors.border.muted} + disabled={disabled} + /> + + + ); +}; + +export default React.memo(SecurityOptionToggle); diff --git a/app/components/UI/SecurityOptionToggle/index.ts b/app/components/UI/SecurityOptionToggle/index.ts new file mode 100644 index 00000000000..e9aee4a1f04 --- /dev/null +++ b/app/components/UI/SecurityOptionToggle/index.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line import/prefer-default-export +export { default as SecurityOptionToggle } from './SecurityOptionToggle'; diff --git a/app/components/UI/SecurityOptionToggle/styles.ts b/app/components/UI/SecurityOptionToggle/styles.ts new file mode 100644 index 00000000000..43276d98726 --- /dev/null +++ b/app/components/UI/SecurityOptionToggle/styles.ts @@ -0,0 +1,32 @@ +/* eslint-disable import/prefer-default-export */ +import { fontStyles } from '../../../styles/common'; +import { StyleSheet } from 'react-native'; + +export const createStyles = (colors: any) => + StyleSheet.create({ + title: { + ...fontStyles.normal, + color: colors.text.default, + fontSize: 20, + lineHeight: 20, + paddingTop: 4, + marginTop: -4, + }, + desc: { + ...fontStyles.normal, + color: colors.text.alternative, + fontSize: 14, + lineHeight: 20, + marginTop: 12, + }, + switchElement: { + marginTop: 18, + alignSelf: 'flex-start', + }, + setting: { + marginTop: 50, + }, + switch: { + alignSelf: 'flex-start', + }, + }); diff --git a/app/components/UI/TurnOffRememberMeModal/TurnOffRememberMeModal.tsx b/app/components/UI/TurnOffRememberMeModal/TurnOffRememberMeModal.tsx new file mode 100644 index 00000000000..c12c1c16112 --- /dev/null +++ b/app/components/UI/TurnOffRememberMeModal/TurnOffRememberMeModal.tsx @@ -0,0 +1,122 @@ +import React, { useState, useRef, useCallback } from 'react'; +import { + View, + TouchableWithoutFeedback, + Keyboard, + SafeAreaView, +} from 'react-native'; +import Text, { TextVariant } from '../../../component-library/components/Text'; +import { OutlinedTextField } from 'react-native-material-textfield'; +import { createStyles } from './styles'; +import ReusableModal, { ReusableModalRef } from '../ReusableModal'; +import WarningExistingUserModal from '../WarningExistingUserModal'; +import { strings } from '../../../../locales/i18n'; +import { useTheme } from '../../../util/theme'; +import Routes from '../../../constants/navigation/Routes'; +import { createNavigationDetails } from '../../..//util/navigation/navUtils'; +import { doesPasswordMatch } from '../../../util/password'; +import Engine from '../../../core/Engine'; +import { logOut } from '../../../actions/user'; +import { setAllowLoginWithRememberMe } from '../../../actions/security'; +import { useDispatch } from 'react-redux'; +import SecureKeychain from '../../../core/SecureKeychain'; +import { TURN_OFF_REMEMBER_ME_MODAL } from '../../../constants/test-ids'; +import { useNavigation } from '@react-navigation/native'; + +export const createTurnOffRememberMeModalNavDetails = createNavigationDetails( + Routes.MODAL.ROOT_MODAL_FLOW, + Routes.MODAL.TURN_OFF_REMEMBER_ME, +); + +const TurnOffRememberMeModal = () => { + const { colors, themeAppearance } = useTheme(); + const { navigate } = useNavigation(); + const styles = createStyles(colors); + const dispatch = useDispatch(); + + const modalRef = useRef(null); + + const [passwordText, setPasswordText] = useState(''); + const [disableButton, setDisableButton] = useState(true); + + const isValidPassword = useCallback( + async (text: string): Promise => { + const response = await doesPasswordMatch(text); + return response.valid; + }, + [], + ); + + const debouncedIsValidPassword = useCallback( + async (text) => setDisableButton(!(await isValidPassword(text))), + [isValidPassword], + ); + + const checkPassword = useCallback( + async (text: string) => { + setPasswordText(text); + debouncedIsValidPassword(text); + }, + [debouncedIsValidPassword], + ); + + const dismissModal = (cb?: () => void): void => + modalRef?.current?.dismissModal(cb); + + const triggerClose = () => dismissModal(); + + const turnOffRememberMeAndLockApp = useCallback(async () => { + const { KeyringController } = Engine.context as any; + await SecureKeychain.resetGenericPassword(); + await KeyringController.setLocked(); + dispatch(setAllowLoginWithRememberMe(false)); + navigate(Routes.ONBOARDING.LOGIN); + dispatch(logOut()); + }, [dispatch, navigate]); + + const disableRememberMe = useCallback(async () => { + dismissModal(async () => await turnOffRememberMeAndLockApp()); + }, [turnOffRememberMeAndLockApp]); + + return ( + + + + + + + {strings('turn_off_remember_me.title')} + + + {strings('turn_off_remember_me.description')} + + + + + + + + ); +}; + +export default React.memo(TurnOffRememberMeModal); diff --git a/app/components/UI/TurnOffRememberMeModal/index.ts b/app/components/UI/TurnOffRememberMeModal/index.ts new file mode 100644 index 00000000000..0ce7d6e4a77 --- /dev/null +++ b/app/components/UI/TurnOffRememberMeModal/index.ts @@ -0,0 +1,2 @@ +/* eslint-disable import/prefer-default-export */ +export { default as TurnOffRememberMeModal } from './TurnOffRememberMeModal'; diff --git a/app/components/UI/TurnOffRememberMeModal/styles.ts b/app/components/UI/TurnOffRememberMeModal/styles.ts new file mode 100644 index 00000000000..506fae7216e --- /dev/null +++ b/app/components/UI/TurnOffRememberMeModal/styles.ts @@ -0,0 +1,25 @@ +/* eslint-disable import/prefer-default-export */ +import { fontStyles } from '../../../styles/common'; +import { StyleSheet } from 'react-native'; + +export const createStyles = (colors: any) => + StyleSheet.create({ + container: { + flex: 1, + }, + areYouSure: { + width: '100%', + justifyContent: 'center', + alignSelf: 'center', + padding: 16, + }, + textStyle: { + paddingVertical: 8, + textAlign: 'center', + }, + input: { + ...fontStyles.normal, + fontSize: 16, + color: colors.text.default, + }, + }); diff --git a/app/components/Views/ChoosePassword/index.js b/app/components/Views/ChoosePassword/index.js index a401a953940..9954b6c7803 100644 --- a/app/components/Views/ChoosePassword/index.js +++ b/app/components/Views/ChoosePassword/index.js @@ -1,7 +1,6 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { - Switch, ActivityIndicator, Alert, Text, @@ -12,7 +11,6 @@ import { Image, InteractionManager, } from 'react-native'; -// eslint-disable-next-line import/no-unresolved import CheckBox from '@react-native-community/checkbox'; import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; import AsyncStorage from '@react-native-community/async-storage'; @@ -27,7 +25,7 @@ import { setLockTime } from '../../../actions/settings'; import StyledButton from '../../UI/StyledButton'; import Engine from '../../../core/Engine'; import Device from '../../../util/device'; -import { fontStyles, colors as importedColors } from '../../../styles/common'; +import { fontStyles } from '../../../styles/common'; import { strings } from '../../../../locales/i18n'; import { getOnboardingNavbarOptions } from '../../UI/Navbar'; import SecureKeychain from '../../../core/SecureKeychain'; @@ -62,6 +60,7 @@ import { IOS_I_UNDERSTAND_BUTTON_ID, ANDROID_I_UNDERSTAND_BUTTON_ID, } from '../../../constants/test-ids'; +import { LoginOptionsSwitch } from '../../UI/LoginOptionsSwitch'; const createStyles = (colors) => StyleSheet.create({ @@ -566,51 +565,17 @@ class ChoosePassword extends PureComponent { }; renderSwitch = () => { - const { biometryType, rememberMe, biometryChoice } = this.state; - const colors = this.context.colors || mockTheme.colors; - const styles = createStyles(colors); - + const { biometryType, biometryChoice } = this.state; + const handleUpdateRememberMe = (rememberMe) => { + this.setState({ rememberMe }); + }; return ( - - {biometryType ? ( - <> - - {strings(`biometrics.enable_${biometryType.toLowerCase()}`)} - - - - - - ) : ( - <> - - {strings(`choose_password.remember_me`)} - - this.setState({ rememberMe })} // eslint-disable-line react/jsx-no-bind - value={rememberMe} - style={styles.biometrySwitch} - trackColor={{ - true: colors.primary.default, - false: colors.border.muted, - }} - thumbColor={importedColors.white} - ios_backgroundColor={colors.border.muted} - testID={'remember-me-toggle'} - /> - - )} - + ); }; diff --git a/app/components/Views/ImportFromSeed/index.js b/app/components/Views/ImportFromSeed/index.js index aedd23ad63f..a6d25a8884d 100644 --- a/app/components/Views/ImportFromSeed/index.js +++ b/app/components/Views/ImportFromSeed/index.js @@ -1,7 +1,6 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { - Switch, ActivityIndicator, Alert, TouchableOpacity, @@ -21,7 +20,7 @@ import { logIn, passwordSet, seedphraseBackedUp } from '../../../actions/user'; import { setLockTime } from '../../../actions/settings'; import StyledButton from '../../UI/StyledButton'; import Engine from '../../../core/Engine'; -import { fontStyles, colors as importedColors } from '../../../styles/common'; +import { fontStyles } from '../../../styles/common'; import { strings } from '../../../../locales/i18n'; import SecureKeychain from '../../../core/SecureKeychain'; import AppConstants from '../../../core/AppConstants'; @@ -62,6 +61,7 @@ import { IMPORT_PASSWORD_CONTAINER_ID, SECRET_RECOVERY_PHRASE_INPUT_BOX_ID, } from '../../../constants/test-ids'; +import { LoginOptionsSwitch } from '../../UI/LoginOptionsSwitch'; const MINIMUM_SUPPORTED_CLIPBOARD_VERSION = 9; @@ -394,10 +394,6 @@ class ImportFromSeed extends PureComponent { } }; - onBiometryChoiceChange = (value) => { - this.setState({ biometryChoice: value }); - }; - clearSecretRecoveryPhrase = async (seed) => { // get clipboard contents const clipboardContents = await Clipboard.getString(); @@ -455,50 +451,16 @@ class ImportFromSeed extends PureComponent { }; renderSwitch = () => { - const colors = this.context.colors || mockTheme.colors; - const styles = createStyles(colors); - - if (this.state.biometryType) { - return ( - - - {strings( - `biometrics.enable_${this.state.biometryType.toLowerCase()}`, - )} - - - - ); - } - + const handleUpdateRememberMe = (rememberMe) => { + this.setState({ rememberMe }); + }; return ( - - - {strings(`choose_password.remember_me`)} - - this.setState({ rememberMe })} // eslint-disable-line react/jsx-no-bind - value={this.state.rememberMe} - style={styles.biometrySwitch} - trackColor={{ - true: colors.primary.default, - false: colors.border.muted, - }} - thumbColor={importedColors.white} - ios_backgroundColor={colors.border.muted} - testID={'remember-me-toggle'} - /> - + ); }; diff --git a/app/components/Views/Login/index.js b/app/components/Views/Login/index.js index a984e71c893..120284c2ab4 100644 --- a/app/components/Views/Login/index.js +++ b/app/components/Views/Login/index.js @@ -1,7 +1,6 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { - Switch, Alert, ActivityIndicator, Text, @@ -17,12 +16,13 @@ import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view import Button from 'react-native-button'; import Engine from '../../../core/Engine'; import StyledButton from '../../UI/StyledButton'; -import { fontStyles, colors as importedColors } from '../../../styles/common'; +import { fontStyles } from '../../../styles/common'; import { strings } from '../../../../locales/i18n'; import SecureKeychain from '../../../core/SecureKeychain'; import FadeOutOverlay from '../../UI/FadeOutOverlay'; import setOnboardingWizardStep from '../../../actions/wizard'; import { logIn, logOut, checkedAuth } from '../../../actions/user'; +import { setAllowLoginWithRememberMe } from '../../../actions/security'; import { connect } from 'react-redux'; import Device from '../../../util/device'; import { OutlinedTextField } from 'react-native-material-textfield'; @@ -49,6 +49,7 @@ import { LOGIN_PASSWORD_ERROR, RESET_WALLET_ID, } from '../../../constants/test-ids'; +import { LoginOptionsSwitch } from '../../UI/LoginOptionsSwitch'; const deviceHeight = Device.getDeviceHeight(); const breakPoint = deviceHeight < 700; @@ -221,6 +222,11 @@ class Login extends PureComponent { * TEMPORARY state for animation control on Nav/App/index.js */ checkedAuth: PropTypes.func, + + /** + * Action to set if the user is using remember me + */ + setAllowLoginWithRememberMe: PropTypes.func, }; state = { @@ -301,6 +307,7 @@ class Login extends PureComponent { const credentials = await SecureKeychain.getGenericPassword(); if (credentials) { this.setState({ rememberMe: true }); + this.props.setAllowLoginWithRememberMe(true); // Restore vault with existing credentials const { KeyringController } = Engine.context; try { @@ -451,51 +458,20 @@ class Login extends PureComponent { }; renderSwitch = () => { - const colors = this.context.colors || mockTheme.colors; - const styles = createStyles(colors); - - if (this.state.biometryType && !this.state.biometryPreviouslyDisabled) { - return ( - - - {strings( - `biometrics.enable_${this.state.biometryType.toLowerCase()}`, - )} - - - this.updateBiometryChoice(biometryChoice) - } // eslint-disable-line react/jsx-no-bind - value={this.state.biometryChoice} - style={styles.biometrySwitch} - trackColor={{ - true: colors.primary.default, - false: colors.border.muted, - }} - thumbColor={importedColors.white} - ios_backgroundColor={colors.border.muted} - /> - - ); - } - + const handleUpdateRememberMe = (rememberMe) => { + this.setState({ rememberMe }); + }; + const shouldRenderBiometricLogin = + this.state.biometryType && !this.state.biometryPreviouslyDisabled + ? this.state.biometryType + : null; return ( - - - {strings(`choose_password.remember_me`)} - - this.setState({ rememberMe })} // eslint-disable-line react/jsx-no-bind - value={this.state.rememberMe} - style={styles.biometrySwitch} - trackColor={{ - true: colors.primary.default, - false: colors.border.muted, - }} - thumbColor={importedColors.white} - ios_backgroundColor={colors.border.muted} - /> - + ); }; @@ -638,6 +614,8 @@ const mapDispatchToProps = (dispatch) => ({ logIn: () => dispatch(logIn()), logOut: () => dispatch(logOut()), checkedAuth: () => dispatch(checkedAuth('login')), + setAllowLoginWithRememberMe: (enabled) => + dispatch(setAllowLoginWithRememberMe(enabled)), }); export default connect(mapStateToProps, mapDispatchToProps)(Login); diff --git a/app/components/Views/ResetPassword/index.js b/app/components/Views/ResetPassword/index.js index 35c58a1c7f6..b575d8db99d 100644 --- a/app/components/Views/ResetPassword/index.js +++ b/app/components/Views/ResetPassword/index.js @@ -2,7 +2,6 @@ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { KeyboardAvoidingView, - Switch, ActivityIndicator, Alert, Text, @@ -14,7 +13,6 @@ import { Image, InteractionManager, } from 'react-native'; -// eslint-disable-next-line import/no-unresolved import CheckBox from '@react-native-community/checkbox'; import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view'; import AsyncStorage from '@react-native-community/async-storage'; @@ -28,11 +26,7 @@ import { setLockTime } from '../../../actions/settings'; import StyledButton from '../../UI/StyledButton'; import Engine from '../../../core/Engine'; import Device from '../../../util/device'; -import { - fontStyles, - baseStyles, - colors as importedColors, -} from '../../../styles/common'; +import { fontStyles, baseStyles } from '../../../styles/common'; import { strings } from '../../../../locales/i18n'; import { getNavigationOptionsTitle } from '../../UI/Navbar'; import SecureKeychain from '../../../core/SecureKeychain'; @@ -62,6 +56,7 @@ import { ANDROID_I_UNDERSTAND_BUTTON_ID, CONFIRM_CHANGE_PASSWORD_INPUT_BOX_ID, } from '../../../constants/test-ids'; +import { LoginOptionsSwitch } from '../../UI/LoginOptionsSwitch'; const createStyles = (colors) => StyleSheet.create({ @@ -231,15 +226,12 @@ const createStyles = (colors) => top: 0, right: 0, }, - // eslint-disable-next-line react-native/no-unused-styles strength_weak: { color: colors.error.default, }, - // eslint-disable-next-line react-native/no-unused-styles strength_good: { color: colors.primary.default, }, - // eslint-disable-next-line react-native/no-unused-styles strength_strong: { color: colors.success.default, }, @@ -548,50 +540,17 @@ class ResetPassword extends PureComponent { }; renderSwitch = () => { - const { biometryType, rememberMe, biometryChoice } = this.state; - const colors = this.context.colors || mockTheme.colors; - const styles = createStyles(colors); - + const { biometryType, biometryChoice } = this.state; + const handleUpdateRememberMe = (rememberMe) => { + this.setState({ rememberMe }); + }; return ( - - {biometryType ? ( - <> - - {strings(`biometrics.enable_${biometryType.toLowerCase()}`)} - - - - - - ) : ( - <> - - {strings(`choose_password.remember_me`)} - - this.setState({ rememberMe })} // eslint-disable-line react/jsx-no-bind - value={rememberMe} - style={styles.biometrySwitch} - trackColor={{ - true: colors.primary.default, - false: colors.border.muted, - }} - thumbColor={importedColors.white} - ios_backgroundColor={colors.border.muted} - /> - - )} - + ); }; diff --git a/app/components/Views/Settings/SecuritySettings/Sections/RememberMeOptionSection.tsx b/app/components/Views/Settings/SecuritySettings/Sections/RememberMeOptionSection.tsx new file mode 100644 index 00000000000..d6b78398027 --- /dev/null +++ b/app/components/Views/Settings/SecuritySettings/Sections/RememberMeOptionSection.tsx @@ -0,0 +1,53 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { SecurityOptionToggle } from '../../../../UI/SecurityOptionToggle'; +import { strings } from '../../../../../../locales/i18n'; +import { checkIfUsingRememberMe } from '../../../../../util/authentication'; +import { useSelector, useDispatch } from 'react-redux'; +import { setAllowLoginWithRememberMe } from '../../../../../actions/security'; +import { useNavigation } from '@react-navigation/native'; +import { createTurnOffRememberMeModalNavDetails } from '../../../..//UI/TurnOffRememberMeModal/TurnOffRememberMeModal'; + +const RememberMeOptionSection = () => { + const { navigate } = useNavigation(); + const allowLoginWithRememberMe = useSelector( + (state: any) => state.security.allowLoginWithRememberMe, + ); + + const [isUsingRememberMe, setIsUsingRememberMe] = useState(false); + useEffect(() => { + const checkIfAlreadyUsingRememberMe = async () => { + const isUsingRememberMeResult = await checkIfUsingRememberMe(); + setIsUsingRememberMe(isUsingRememberMeResult); + }; + checkIfAlreadyUsingRememberMe(); + }, []); + + const dispatch = useDispatch(); + + const toggleRememberMe = useCallback( + (value: boolean) => { + dispatch(setAllowLoginWithRememberMe(value)); + }, + [dispatch], + ); + + const onValueChanged = useCallback( + (enabled: boolean) => { + isUsingRememberMe + ? navigate(...createTurnOffRememberMeModalNavDetails()) + : toggleRememberMe(enabled); + }, + [isUsingRememberMe, navigate, toggleRememberMe], + ); + + return ( + onValueChanged(value)} + /> + ); +}; + +export default React.memo(RememberMeOptionSection); diff --git a/app/components/Views/Settings/SecuritySettings/index.js b/app/components/Views/Settings/SecuritySettings/index.js index adf6dc48626..01fc3879057 100644 --- a/app/components/Views/Settings/SecuritySettings/index.js +++ b/app/components/Views/Settings/SecuritySettings/index.js @@ -65,6 +65,7 @@ import { import { LEARN_MORE_URL } from '../../../../constants/urls'; import DeleteMetaMetricsData from './Sections/DeleteMetaMetricsData'; import DeleteWalletData from './Sections/DeleteWalletData'; +import RememberMeOptionSection from './Sections/RememberMeOptionSection'; const isIos = Device.isIos(); @@ -1192,6 +1193,7 @@ class Settings extends PureComponent { {this.renderPasswordSection()} {this.renderAutoLockSection()} {biometryType && this.renderBiometricOptionsSection()} + {biometryType && !biometryChoice && this.renderDevicePasscodeSection()} diff --git a/app/components/Views/Settings/SecuritySettings/index.test.tsx b/app/components/Views/Settings/SecuritySettings/index.test.tsx index d80b1c21aee..2dbae2c63c6 100644 --- a/app/components/Views/Settings/SecuritySettings/index.test.tsx +++ b/app/components/Views/Settings/SecuritySettings/index.test.tsx @@ -27,6 +27,9 @@ const initialState = { }, }, }, + security: { + allowLoginWithRememberMe: true, + }, }; const store = mockStore(initialState); diff --git a/app/constants/navigation/Routes.ts b/app/constants/navigation/Routes.ts index 77453521f3a..03d27d4f5ba 100644 --- a/app/constants/navigation/Routes.ts +++ b/app/constants/navigation/Routes.ts @@ -19,6 +19,7 @@ const Routes = { ROOT_MODAL_FLOW: 'RootModalFlow', MODAL_CONFIRMATION: 'ModalConfirmation', WHATS_NEW: 'WhatsNewModal', + TURN_OFF_REMEMBER_ME: 'TurnOffRememberMeModal', }, ONBOARDING: { ROOT_NAV: 'OnboardingRootNav', diff --git a/app/constants/test-ids.js b/app/constants/test-ids.js index 32f7683fddd..353361cdcb5 100644 --- a/app/constants/test-ids.js +++ b/app/constants/test-ids.js @@ -67,3 +67,21 @@ export const WHATS_NEW_MODAL_CLOSE_BUTTON_ID = 'whats-new-modal-close-button'; export const WHATS_NEW_MODAL_GOT_IT_BUTTON_ID = 'whats-new-modal-got-it-button'; export const INPUT_NETWORK_NAME = 'input-network-name'; + +// Component library test ids +export const FAVICON_AVATAR_IMAGE_ID = 'favicon-avatar-image'; +export const NETWORK_AVATAR_IMAGE_ID = 'network-avatar-image'; +export const CHECKBOX_ICON_ID = 'checkbox-icon'; +export const SELECTABLE_LIST_ITEM_OVERLAY_ID = 'selectable-list-item-overlay'; +export const CONFIRMATION_MODAL_NORMAL_BUTTON_ID = + 'confirmation-modal-normal-button'; +export const CONFIRMATION_MODAL_DANGER_BUTTON_ID = + 'confirmation-modal-danger-button'; +export const TOKEN_AVATAR_IMAGE_ID = 'token-avatar-image'; +export const STACKED_AVATARS_OVERFLOW_COUNTER = + 'stacked-avatar-overflow-counter'; + +// LoginOptionsSwitch +export const LOGIN_WITH_BIOMETRICS_SWITCH = 'login-with-biometrics-switch'; +export const LOGIN_WITH_REMEMBER_ME_SWITCH = 'login-with-remember-me-switch'; +export const TURN_OFF_REMEMBER_ME_MODAL = 'TurnOffRememberMeConfirm'; diff --git a/app/reducers/index.js b/app/reducers/index.js index 6c08bd65a46..fb99777e178 100644 --- a/app/reducers/index.js +++ b/app/reducers/index.js @@ -17,6 +17,7 @@ import collectiblesReducer from './collectibles'; import recentsReducer from './recents'; import navigationReducer from './navigation'; import networkOnboardReducer from './networkSelector'; +import securityReducer from './security'; import { combineReducers } from 'redux'; const rootReducer = combineReducers({ @@ -39,6 +40,7 @@ const rootReducer = combineReducers({ infuraAvailability: infuraAvailabilityReducer, navigation: navigationReducer, networkOnboarded: networkOnboardReducer, + security: securityReducer, }); export default rootReducer; diff --git a/app/reducers/security/index.ts b/app/reducers/security/index.ts new file mode 100644 index 00000000000..7119be56407 --- /dev/null +++ b/app/reducers/security/index.ts @@ -0,0 +1,23 @@ +/* eslint-disable @typescript-eslint/default-param-last */ +import { ActionType, Action } from '../../actions/security'; +import { SecuritySettingsState } from '../../actions/security/state'; + +const initialState: Readonly = { + allowLoginWithRememberMe: false, +}; + +const securityReducer = ( + state: SecuritySettingsState = initialState, + action: Action, +): SecuritySettingsState => { + switch (action.type) { + case ActionType.SET_ALLOW_LOGIN_WITH_REMEMBER_ME: + return { + allowLoginWithRememberMe: action.enabled, + }; + default: + return state; + } +}; + +export default securityReducer; diff --git a/app/util/authentication/checkIfUsingRememberMe.ts b/app/util/authentication/checkIfUsingRememberMe.ts new file mode 100644 index 00000000000..e4ec5f12ce0 --- /dev/null +++ b/app/util/authentication/checkIfUsingRememberMe.ts @@ -0,0 +1,45 @@ +/* eslint-disable import/prefer-default-export */ +import SecureKeychain from '../../core/SecureKeychain'; +import Engine from '../../core/Engine'; +import AsyncStorage from '@react-native-community/async-storage'; +import { + BIOMETRY_CHOICE, + PASSCODE_CHOICE, + TRUE, +} from '../../constants/storage'; +import Logger from '../../util/Logger'; + +/** + * Checks to see if the user has enabled Remember Me and logs + * into the application if it is enabled. + */ +const checkIfUsingRememberMe = async (): Promise => { + const biometryChoice = await AsyncStorage.getItem(BIOMETRY_CHOICE); + // since we do not allow remember me to be used with biometrics we can eagerly return false + if (biometryChoice) { + return false; + } + // since we do not allow remember me to be used with passcode we can eagerly return false + const passcodeChoice = await AsyncStorage.getItem(PASSCODE_CHOICE); + if (passcodeChoice !== '' && passcodeChoice === TRUE) { + return false; + } + const credentials = await SecureKeychain.getGenericPassword(); + if (credentials) { + // Restore vault with existing credentials + const { KeyringController } = Engine.context as any; + try { + // this is being used a hack to verify that the credentials are valid + await KeyringController.exportSeedPhrase(credentials.password); + return true; + } catch (error) { + Logger.log( + 'Error in checkIfUsingRememberMe while calling KeyringController.exportSeedPhrase', + error, + ); + return false; + } + } else return false; +}; + +export default checkIfUsingRememberMe; diff --git a/app/util/authentication/index.ts b/app/util/authentication/index.ts new file mode 100644 index 00000000000..e47ba7dcb66 --- /dev/null +++ b/app/util/authentication/index.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line import/prefer-default-export +export { default as checkIfUsingRememberMe } from './checkIfUsingRememberMe'; diff --git a/app/util/password/index.js b/app/util/password/index.js deleted file mode 100644 index e630130c355..00000000000 --- a/app/util/password/index.js +++ /dev/null @@ -1,18 +0,0 @@ -export const MIN_PASSWORD_LENGTH = 8; -export const getPasswordStrengthWord = (strength) => { - switch (strength) { - case 0: - return 'weak'; - case 1: - return 'weak'; - case 2: - return 'weak'; - case 3: - return 'good'; - case 4: - return 'strong'; - } -}; - -export const passwordRequirementsMet = (password) => - password.length >= MIN_PASSWORD_LENGTH; diff --git a/app/util/password/index.ts b/app/util/password/index.ts new file mode 100644 index 00000000000..91cfe71b28c --- /dev/null +++ b/app/util/password/index.ts @@ -0,0 +1,69 @@ +import SecureKeychain from '../../core/SecureKeychain'; +import Engine from '../../core/Engine'; + +export const MIN_PASSWORD_LENGTH = 8; +type PasswordStrength = 0 | 1 | 2 | 3 | 4; + +export const getPasswordStrengthWord = (strength: PasswordStrength) => { + switch (strength) { + case 0: + return 'weak'; + case 1: + return 'weak'; + case 2: + return 'weak'; + case 3: + return 'good'; + case 4: + return 'strong'; + } +}; + +export const passwordRequirementsMet = (password: string) => + password.length >= MIN_PASSWORD_LENGTH; + +interface PasswordValidationResponse { + valid: boolean; + message: string; +} + +export const doesPasswordMatch = async ( + input: string, +): Promise => { + try { + // first try to get the stored password + const credentials = await SecureKeychain.getGenericPassword(); + if (credentials) { + try { + // then we verify if the stored password matches the one that can decrypt the vault + const { KeyringController } = Engine.context as any; + await KeyringController.exportSeedPhrase(credentials.password); + // now that we are confident that the user is logged in, we can test that the input matches + if (input === credentials.password) { + return { + valid: true, + message: 'The input matches the stored password', + }; + } + return { + valid: false, + message: 'The input does not match the stored password', + }; + } catch (error: any) { + return { + valid: false, + message: error.toString(), + }; + } + } + return { + valid: false, + message: 'no password stored', + }; + } catch (error: any) { + return { + valid: false, + message: error.toString(), + }; + } +}; diff --git a/app/util/testSetup.js b/app/util/testSetup.js index 4b4da4b3942..df02fcd8e26 100644 --- a/app/util/testSetup.js +++ b/app/util/testSetup.js @@ -88,9 +88,24 @@ jest.mock('../core/Engine', () => ({ }, })); -jest.mock('react-native-keychain', () => ({ - getSupportedBiometryType: () => Promise.resolve('FaceId'), -})); +const keychainMock = { + SECURITY_LEVEL_ANY: 'MOCK_SECURITY_LEVEL_ANY', + SECURITY_LEVEL_SECURE_SOFTWARE: 'MOCK_SECURITY_LEVEL_SECURE_SOFTWARE', + SECURITY_LEVEL_SECURE_HARDWARE: 'MOCK_SECURITY_LEVEL_SECURE_HARDWARE', + setGenericPassword: jest.fn(), + getGenericPassword: jest.fn(), + resetGenericPassword: jest.fn(), + BIOMETRY_TYPE: { + TOUCH_ID: 'TouchID', + FACE_ID: 'FaceID', + FINGERPRINT: 'Fingerprint', + FACE: 'Face', + IRIS: 'Iris', + }, + getSupportedBiometryType: jest.fn().mockReturnValue('FaceID'), +}; + +jest.mock('react-native-keychain', () => keychainMock); jest.mock('react-native-share', () => 'RNShare'); jest.mock('react-native-branch', () => ({ BranchSubscriber: () => { diff --git a/ios/Podfile.lock b/ios/Podfile.lock index b9855a51b93..a4881dfa174 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -841,4 +841,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 5759da19ce141e5f8aca3a4f56ac232baa34e957 -COCOAPODS: 1.11.3 +COCOAPODS: 1.11.2 diff --git a/locales/languages/de.json b/locales/languages/de.json index c728007b4b8..73573597a1c 100644 --- a/locales/languages/de.json +++ b/locales/languages/de.json @@ -1996,5 +1996,15 @@ "token_detection_mainnet_title": "Die Token-Erkennung ist aktiviert, so dass viele Token automatisch in Ihrem Wallet auftauchen werden.", "token_detection_mainnet_link": "Sie können Token auch manuell hinzufügen.", "or": "oder" + }, + "remember_me": { + "enable_remember_me": "„Angemeldet bleiben“ aktivieren", + "enable_remember_me_description": "Wenn „Angemeldet bleiben“ aktiviert ist, kann jeder, der Zugriff zu Ihrem Handy hat, auf Ihr MetaMask-Konto zugreifen." + }, + "turn_off_remember_me": { + "title": "Geben Sie Ihr Passwort ein, um „Angemeldet bleiben“ zu deaktivieren", + "placeholder": "Passwort", + "description": "Wenn Sie diese Option deaktivieren, benötigen Sie von jetzt an Ihr Passwort, um MetaMask zu entsperren.", + "action": "„Angemeldet bleiben“ deaktivieren" } } diff --git a/locales/languages/el.json b/locales/languages/el.json index 2606a137d67..178a6c713d9 100644 --- a/locales/languages/el.json +++ b/locales/languages/el.json @@ -1996,5 +1996,15 @@ "token_detection_mainnet_title": "Ο εντοπισμός token είναι ενεργοποιημένος και πολλά token θα εμφανιστούν αυτόματα στο πορτοφόλι σας.", "token_detection_mainnet_link": "Μπορείτε επίσης να προσθέσετε token μη αυτόματα.", "or": "ή" + }, + "remember_me": { + "enable_remember_me": "Δοκιμάστε να ενεργοποιήσετε το Θυμήσου με", + "enable_remember_me_description": "Όταν το «Θυμήσου με» είναι ενεργοποιημένο, οποιοσδήποτε με πρόσβαση στο κινητό σας μπορεί να έχει πρόσβαση στον λογαριασμό σας MetaMask." + }, + "turn_off_remember_me": { + "title": "Εισαγάγετε τον κωδικό πρόσβασής σας για να απενεργοποιήσετε το «Θυμήσου με»", + "placeholder": "Κωδικός πρόσβασης", + "description": "Εάν απενεργοποιήσετε αυτή την επιλογή, από εδώ και πέρα θα χρειάζεστε τον κωδικό πρόσβασής σας για να ξεκλειδώσετε το MetaMask.", + "action": "Απενεργοποίηση του «Θυμήσου με»" } } diff --git a/locales/languages/es.json b/locales/languages/es.json index 109c5fcbfee..c85e8634742 100644 --- a/locales/languages/es.json +++ b/locales/languages/es.json @@ -1996,5 +1996,15 @@ "token_detection_mainnet_title": "La detección de tokens está habilitada, por lo que muchos aparecerán automáticamente en su billetera.", "token_detection_mainnet_link": "También puede agregar los tokens manualmente.", "or": "o" + }, + "remember_me": { + "enable_remember_me": "Activar Recordarme", + "enable_remember_me_description": "Cuando Recordarme está activado, cualquier persona con acceso a su teléfono puede acceder a su cuenta de MetaMask." + }, + "turn_off_remember_me": { + "title": "Ingrese su contraseña para desactivar Recordarme", + "placeholder": "Contraseña", + "description": "Si desactiva esta opción, necesitará su contraseña para desbloquear MetaMask de ahora en adelante.", + "action": "Desactivar Recordarme" } } diff --git a/locales/languages/fr.json b/locales/languages/fr.json index bffdce15cbf..aff5f6bd9c4 100644 --- a/locales/languages/fr.json +++ b/locales/languages/fr.json @@ -1996,5 +1996,15 @@ "token_detection_mainnet_title": "La détection des jetons est activée, ce qui signifie que de nombreux jetons apparaîtront automatiquement dans votre portefeuille.", "token_detection_mainnet_link": "Vous pouvez également ajouter des jetons manuellement.", "or": "ou" + }, + "remember_me": { + "enable_remember_me": "Activer l'option « Se souvenir de moi »", + "enable_remember_me_description": "Lorsque l'option « Se souvenir de moi » est activée, toute personne ayant accès à votre téléphone peut accéder à votre compte MetaMask." + }, + "turn_off_remember_me": { + "title": "Saisissez votre mot de passe pour désactiver l'option « Se souvenir de moi »", + "placeholder": "Mot de passe", + "description": "Si vous désactivez cette option, la saisie du mot de passe sera requise pour déverrouiller MetaMask.", + "action": "Désactiver l'option « Se souvenir de moi »" } } diff --git a/locales/languages/hi.json b/locales/languages/hi.json index 7590edae8b2..ec44bf56e02 100644 --- a/locales/languages/hi.json +++ b/locales/languages/hi.json @@ -1996,5 +1996,15 @@ "token_detection_mainnet_title": "Token detection is enabled so many tokens will automatically show up in your wallet.", "token_detection_mainnet_link": "You can also add tokens manually.", "or": "or" + }, + "remember_me": { + "enable_remember_me": "रिमेंबर मी चालू करें", + "enable_remember_me_description": "जब रिमेंबर मी चालू रहेगा, आपका फोन एक्सेस करने वाला कोई भी व्यक्ति आपके MetaMask अकाउंट को एक्सेस कर सकता है।" + }, + "turn_off_remember_me": { + "title": "रिमेंबर मी को बंद करने के लिए अपना पासवर्ड दर्ज करें", + "placeholder": "पासवर्ड", + "description": "अगर आप इस ऑप्शन को बंद करते हैं, तो अब से आपको MetaMask अनलॉक करने के लिए अपने पासवर्ड की ज़रूरत होगी।", + "action": "रिमेंबर मी बंद करें" } } diff --git a/locales/languages/id.json b/locales/languages/id.json index 5453eb68af3..f5225d49628 100644 --- a/locales/languages/id.json +++ b/locales/languages/id.json @@ -1996,5 +1996,15 @@ "token_detection_mainnet_title": "Deteksi token diaktifkan sehingga banyak token akan muncul di dompet Anda secara otomatis.", "token_detection_mainnet_link": "Anda juga dapat menambahkan token secara manual.", "or": "atau" + }, + "remember_me": { + "enable_remember_me": "Aktifkan Ingatkan saya", + "enable_remember_me_description": "Saat Ingatkan saya aktif, siapa pun yang memiliki akses ke ponsel Anda dapat mengakses akun MetaMask Anda." + }, + "turn_off_remember_me": { + "title": "Masukkan kata sandi Anda untuk menonaktifkan Ingatkan saya", + "placeholder": "Kata sandi", + "description": "Jika Anda menonaktifkan opsi ini, Anda memerlukan kata sandi untuk membuka MetaMask mulai sekarang.", + "action": "Nonaktifkan Ingatkan saya" } } diff --git a/locales/languages/ja.json b/locales/languages/ja.json index deff30532b4..1e88e058015 100644 --- a/locales/languages/ja.json +++ b/locales/languages/ja.json @@ -1996,5 +1996,15 @@ "token_detection_mainnet_title": "トークン検出が有効になっているため、多くのトークンが自動的にウォレットに表示されます。", "token_detection_mainnet_link": "手動でトークンを追加することもできます。", "or": "または" + }, + "remember_me": { + "enable_remember_me": "認証情報の保存を有効にする", + "enable_remember_me_description": "認証情報の保存を有効にすると、携帯にアクセスできる人は誰でも MetaMask アカウントにアクセスできるようになります。" + }, + "turn_off_remember_me": { + "title": "認証情報の保存を無効にするには、パスワードを入力してください", + "placeholder": "パスワード", + "description": "このオプションを無効にすると、今後 MetaMask のロックを解除するのにパスワードが必要になります。", + "action": "認証情報の保存を無効にする" } } diff --git a/locales/languages/ko.json b/locales/languages/ko.json index 4dba3d3eb73..8d1e0ffb0d4 100644 --- a/locales/languages/ko.json +++ b/locales/languages/ko.json @@ -1996,5 +1996,15 @@ "token_detection_mainnet_title": "토큰 감지가 활성화되어 여러 토큰이 자동으로 지갑에 나타납니다.", "token_detection_mainnet_link": "또한 수동으로 토큰을 추가할 수도 있습니다.", "or": "또는" + }, + "remember_me": { + "enable_remember_me": "로그인 정보 저장", + "enable_remember_me_description": "로그인 정보를 저장하면 회원님의 휴대폰을 이용하는 모든 사람이 회원님의 MetaMask 계정에 액세스할 수 있습니다." + }, + "turn_off_remember_me": { + "title": "로그인 정보 저장 기능을 끄려면 비밀번호를 입력하세요", + "placeholder": "비밀번호", + "description": "이 기능을 끄면 지금부터 비밀번호를 사용하여 MetaMask를 잠금 해제해야 합니다.", + "action": "로그인 정보 저장 끄기" } } diff --git a/locales/languages/pt.json b/locales/languages/pt.json index 91dda89691e..0b1e5a8a834 100644 --- a/locales/languages/pt.json +++ b/locales/languages/pt.json @@ -1996,5 +1996,15 @@ "token_detection_mainnet_title": "Com a detecção ativada, muitos tokens serão automaticamente exibidos em sua carteira.", "token_detection_mainnet_link": "Você também pode adicionar tokens manualmente.", "or": "ou" + }, + "remember_me": { + "enable_remember_me": "Ativar \"Lembrar de mim\"", + "enable_remember_me_description": "Quando \"Lembrar de mim\" estiver ativado, qualquer pessoa com acesso ao seu telefone poderá acessar sua conta da MetaMask." + }, + "turn_off_remember_me": { + "title": "Insira sua senha para desativar \"Lembrar de mim\"", + "placeholder": "Senha", + "description": "Se desativar essa opção, você precisará de sua senha para desbloquear a MetaMask a partir de agora.", + "action": "Desativar \"Lembrar de mim\"" } } diff --git a/locales/languages/ru.json b/locales/languages/ru.json index f8232b79fe0..9d3853401ea 100644 --- a/locales/languages/ru.json +++ b/locales/languages/ru.json @@ -1996,5 +1996,15 @@ "token_detection_mainnet_title": "Обнаружение токенов включено, поэтому многие токены автоматически появятся в вашем кошельке.", "token_detection_mainnet_link": "Вы также можете добавлять токены вручную.", "or": "или" + }, + "remember_me": { + "enable_remember_me": "Включить \"Запомнить меня\"", + "enable_remember_me_description": "Когда функция \"Запомнить меня\" включена, любой, у кого есть доступ к вашему телефону, может получить доступ к вашему счету MetaMask." + }, + "turn_off_remember_me": { + "title": "Введите пароль, чтобы отключить \"Запомнить меня\"", + "placeholder": "Пароль", + "description": "Если вы отключите эту опцию, с этого момента вам потребуется пароль для разблокировки MetaMask.", + "action": "Выключить \"Запомнить меня\"" } } diff --git a/locales/languages/tl.json b/locales/languages/tl.json index d9e3d864a24..6c96edc0f0d 100644 --- a/locales/languages/tl.json +++ b/locales/languages/tl.json @@ -1996,5 +1996,15 @@ "token_detection_mainnet_title": "Pinagana ang pagtuklas ng token kaya maraming mga token ang awtomatikong lalabas sa iyong wallet.", "token_detection_mainnet_link": "Maaari ka ring magdagdag ng mga token ng manu-mano.", "or": "o" + }, + "remember_me": { + "enable_remember_me": "I-on ang Tandaan ako", + "enable_remember_me_description": "Kapag naka-on ang tandaan ako, sinumang may access sa iyong telepono ay makaka-access sa iyong MetaMask account." + }, + "turn_off_remember_me": { + "title": "Ilagay ang iyong password para i-off ang Tandaan ako", + "placeholder": "Password", + "description": "Kung in-off mo ang opsyong ito, kakailanganin mo ang iyong password upang i-unlock ang MetaMask mula ngayon.", + "action": "I-off ang Tandaan ako" } } diff --git a/locales/languages/tr.json b/locales/languages/tr.json index c3dacabdd60..648f1c69090 100644 --- a/locales/languages/tr.json +++ b/locales/languages/tr.json @@ -1996,5 +1996,15 @@ "token_detection_mainnet_title": "Token algılama etkinleştirildi böylece pek çok token otomatik olarak cüzdanınızda gösterilecek.", "token_detection_mainnet_link": "Manuel olarak da ekleyebilirsiniz.", "or": "veya" + }, + "remember_me": { + "enable_remember_me": "Beni Hatırla özelliğini aç", + "enable_remember_me_description": "Beni Hatırla özelliği açıldığında telefonunuza erişimi olan herkes MetaMask hesabınıza erişim sağlayabilir." + }, + "turn_off_remember_me": { + "title": "Beni Hatırla özelliğini kapatmak için parolanızı girin", + "placeholder": "Parola", + "description": "Bu seçeneği kapatırsanız şu andan itibaren MetaMask'in kilidini açmak için şifrenizi girmeniz gerekecektir.", + "action": "Beni Hatırla özelliğini kapat" } } diff --git a/locales/languages/vi.json b/locales/languages/vi.json index d45dc32451a..2a80c1576f3 100644 --- a/locales/languages/vi.json +++ b/locales/languages/vi.json @@ -1996,5 +1996,15 @@ "token_detection_mainnet_title": "Đã bật tính năng phát hiện token nên nhiều token sẽ tự động hiển thị trong ví của bạn.", "token_detection_mainnet_link": "Bạn cũng có thể thêm token theo cách thủ công.", "or": "hoặc" + }, + "remember_me": { + "enable_remember_me": "Bật Ghi nhớ", + "enable_remember_me_description": "Khi bật Ghi nhớ, bất kỳ ai có quyền truy cập vào điện thoại của bạn đều có thể truy cập vào tài khoản MetaMask của bạn." + }, + "turn_off_remember_me": { + "title": "Nhập mật khẩu để tắt Ghi nhớ", + "placeholder": "Mật khẩu", + "description": "Nếu bạn tắt tùy chọn này, bạn sẽ cần nhập mật khẩu để mở khóa MetaMask kể từ bây giờ.", + "action": "Tắt Ghi nhớ" } } diff --git a/locales/languages/zh.json b/locales/languages/zh.json index d44ed57a42a..8c15677bb87 100644 --- a/locales/languages/zh.json +++ b/locales/languages/zh.json @@ -1996,5 +1996,15 @@ "token_detection_mainnet_title": "已启用代币检测,因此将会有许多代币自动显示在您的钱包中。", "token_detection_mainnet_link": "您也可以手动添加代币。", "or": "或" + }, + "remember_me": { + "enable_remember_me": "打开“记住我”", + "enable_remember_me_description": "在“记住我”功能打开时,任何有访问您手机权限的人都可以访问您的MetaMask账户。" + }, + "turn_off_remember_me": { + "title": "输入密码以关闭“记住我”", + "placeholder": "密码", + "description": "如果关闭此选项,从现在起,您需要使用密码来解锁MetaMask。", + "action": "关闭“记住我”" } } diff --git a/package.json b/package.json index b73ff064dc8..a635f42db54 100644 --- a/package.json +++ b/package.json @@ -280,6 +280,7 @@ "@types/jest": "^27.0.1", "@types/react": "^17.0.11", "@types/react-native": "^0.64.10", + "@types/react-native-material-textfield": "^0.16.5", "@types/react-native-vector-icons": "^6.4.8", "@types/react-native-video": "^5.0.13", "@types/redux-mock-store": "^1.0.3", diff --git a/yarn.lock b/yarn.lock index 3931a99890a..0dc1b525af5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3714,6 +3714,14 @@ "@types/react" "*" "@types/reactcss" "*" +"@types/react-native-material-textfield@^0.16.5": + version "0.16.5" + resolved "https://registry.yarnpkg.com/@types/react-native-material-textfield/-/react-native-material-textfield-0.16.5.tgz#fe91ef6339c282668f6eae24ca09a69f0a974974" + integrity sha512-QS3QzYiIE8DcaMp0ZALI3CQYsSn2mC8jjJAcbDleDfMeQ3+CD49kRpvKRKrgBkq7n4eRTZ2/KvaEKvb12sJ7Iw== + dependencies: + "@types/react" "*" + "@types/react-native" "*" + "@types/react-native-vector-icons@^6.4.8": version "6.4.8" resolved "https://registry.yarnpkg.com/@types/react-native-vector-icons/-/react-native-vector-icons-6.4.8.tgz#7dd9740f36a71e98c484b9ea12155940c85cedc2"