From a9579fff3135447d529953d6677698c5bdd51381 Mon Sep 17 00:00:00 2001 From: Thuc Nguyen Duy Date: Thu, 17 Aug 2023 17:36:41 +0700 Subject: [PATCH] [issue-444, issue-430, issue-910] Use Keychain for biometric auth, new login screen, update reset account --- android/app/src/main/AndroidManifest.xml | 1 + ios/Podfile.lock | 16 +- package.json | 4 +- src/AppNavigator.tsx | 31 +- src/AppNew.tsx | 127 ++++---- src/assets/fingerprint-simple.svg | 10 + src/assets/index.ts | 4 + src/assets/subwallet-styled.svg | 3 + .../common/Field/Password/InlinePassword.tsx | 113 ++++++++ src/components/common/Field/Password/index.ts | 1 + .../common/Field/Password/styles/index.ts | 42 +++ .../common/ForgotPasswordModal/index.tsx | 16 +- .../common/Modal/UnlockModal/index.tsx | 141 +++++++-- .../common/Modal/UnlockModal/style/index.ts | 4 + .../design-system-ui/modal/ActionHeader.tsx | 38 +++ .../design-system-ui/modal/ModalBaseV2.tsx | 14 +- .../design-system-ui/modal/SwModal.tsx | 4 +- .../design-system-ui/modal/index.tsx | 2 + .../design-system-ui/modal/style/index.ts | 13 + src/hooks/modal/useUnlockModal.ts | 13 +- src/hooks/useAppLock.ts | 31 +- src/messaging/index.ts | 4 + src/routes/index.ts | 2 - src/screens/Home/Crypto/ServiceModal.tsx | 3 +- src/screens/Home/index.tsx | 4 +- src/screens/LockScreen.tsx | 186 ------------ .../ChangeMasterPassword/index.tsx | 12 + .../CreateMasterPassword/index.tsx | 7 + src/screens/MasterPassword/Login/index.tsx | 274 +++++++++++++++--- .../MasterPassword/Login/styles/index.ts | 28 +- src/screens/Settings/Security/PinCode.tsx | 47 --- .../Settings/Security/PinCodeScreen.tsx | 109 ------- src/screens/Settings/Security/index.tsx | 126 ++++---- src/screens/Settings/index.tsx | 13 +- src/stores/Browser.ts | 8 + src/stores/MobileSettings.ts | 29 +- src/stores/types.ts | 17 +- src/utils/account.ts | 52 ++++ src/utils/i18n/en_US.ts | 5 + src/utils/i18n/vi_VN.ts | 5 + src/utils/i18n/zh_CN.ts | 5 + src/utils/permission/biometric.ts | 46 +++ yarn.lock | 18 +- 43 files changed, 980 insertions(+), 648 deletions(-) create mode 100644 src/assets/fingerprint-simple.svg create mode 100644 src/assets/subwallet-styled.svg create mode 100644 src/components/common/Field/Password/InlinePassword.tsx create mode 100644 src/components/common/Field/Password/index.ts create mode 100644 src/components/common/Field/Password/styles/index.ts create mode 100644 src/components/design-system-ui/modal/ActionHeader.tsx delete mode 100644 src/screens/LockScreen.tsx delete mode 100644 src/screens/Settings/Security/PinCode.tsx delete mode 100644 src/screens/Settings/Security/PinCodeScreen.tsx create mode 100644 src/utils/permission/biometric.ts diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 13f1144b8..eebc00f4c 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -6,6 +6,7 @@ + 1.0) - SDWebImage/Core (~> 5.10) - SocketRocket (0.6.1) - - TouchID (4.4.1): - - React - Yoga (1.14.0) - YogaKit (1.18.1): - Yoga (~> 1.14) @@ -613,6 +613,7 @@ DEPENDENCIES: - react-native-restart (from `../node_modules/react-native-restart`) - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) - "react-native-segmented-control (from `../node_modules/@react-native-community/segmented-control`)" + - react-native-sensitive-info (from `../node_modules/react-native-sensitive-info`) - "react-native-slider (from `../node_modules/@react-native-community/slider`)" - react-native-splash-screen (from `../node_modules/react-native-splash-screen`) - react-native-static-server (from `../node_modules/react-native-static-server`) @@ -651,7 +652,6 @@ DEPENDENCIES: - RNScreens (from `../node_modules/react-native-screens`) - RNSVG (from `../node_modules/react-native-svg`) - RNVectorIcons (from `../node_modules/react-native-vector-icons`) - - TouchID (from `../node_modules/react-native-touch-id`) - Yoga (from `../node_modules/react-native/ReactCommon/yoga`) SPEC REPOS: @@ -742,6 +742,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-safe-area-context" react-native-segmented-control: :path: "../node_modules/@react-native-community/segmented-control" + react-native-sensitive-info: + :path: "../node_modules/react-native-sensitive-info" react-native-slider: :path: "../node_modules/@react-native-community/slider" react-native-splash-screen: @@ -818,8 +820,6 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-svg" RNVectorIcons: :path: "../node_modules/react-native-vector-icons" - TouchID: - :path: "../node_modules/react-native-touch-id" Yoga: :path: "../node_modules/react-native/ReactCommon/yoga" @@ -872,6 +872,7 @@ SPEC CHECKSUMS: react-native-restart: 45c8dca02491980f2958595333cbccd6877cb57e react-native-safe-area-context: 9697629f7b2cda43cf52169bb7e0767d330648c2 react-native-segmented-control: 65df6cd0619b780b3843d574a72d4c7cec396097 + react-native-sensitive-info: d44e909d065f9c0e15734245e5dd6a24b82e3dcd react-native-slider: 33b8d190b59d4f67a541061bb91775d53d617d9d react-native-splash-screen: 4312f786b13a81b5169ef346d76d33bc0c6dc457 react-native-static-server: 201b2a945a35096be3ae7f43e367c65bcbd61343 @@ -903,7 +904,7 @@ SPEC CHECKSUMS: RNFS: 4ac0f0ea233904cb798630b3c077808c06931688 RNGestureHandler: dec4645026e7401a0899f2846d864403478ff6a5 RNInAppBrowser: e36d6935517101ccba0e875bac8ad7b0cb655364 - RNKeychain: ff836453cba46938e0e9e4c22e43d43fa2c90333 + RNKeychain: a65256b6ca6ba6976132cc4124b238a5b13b3d9c RNPermissions: 8231416ed851ad4f9ddc220494467c8f1f79c5df RNQrGenerator: 90461ba3ca88c1d38ef73da50fade35d9648215d RNReanimated: f186e85d9f28c9383d05ca39e11dd194f59093ec @@ -913,7 +914,6 @@ SPEC CHECKSUMS: SDWebImage: a7f831e1a65eb5e285e3fb046a23fcfbf08e696d SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 - TouchID: ba4c656d849cceabc2e4eef722dea5e55959ecf4 Yoga: 39310a10944fc864a7550700de349183450f8aaa YogaKit: f782866e155069a2cca2517aafea43200b01fd5a ZXingObjC: fdbb269f25dd2032da343e06f10224d62f537bdb diff --git a/package.json b/package.json index 745628935..4824bb03f 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,7 @@ "react-native-gesture-handler": "^2.9.0", "react-native-image-picker": "^5.0.1", "react-native-inappbrowser-reborn": "^3.7.0", - "react-native-keychain": "^8.1.1", + "react-native-keychain": "^8.1.2", "react-native-linear-gradient": "^2.6.2", "react-native-localization": "^2.3.1", "react-native-mmkv": "^2.10.1", @@ -105,6 +105,7 @@ "react-native-restart": "^0.0.24", "react-native-safe-area-context": "^4.5.0", "react-native-screens": "^3.19.0", + "react-native-sensitive-info": "^6.0.0-alpha.9", "react-native-skeleton-placeholder": "^5.2.4", "react-native-splash-screen": "^3.3.0", "react-native-static-server": "^0.5.0", @@ -113,7 +114,6 @@ "react-native-svg-transformer": "^1.0.0", "react-native-tab-view": "^3.5.2", "react-native-toast-notifications": "^3.3.1", - "react-native-touch-id": "^4.4.1", "react-native-vector-icons": "^9.2.0", "react-native-version-number": "^0.3.6", "react-native-video": "^5.2.1", diff --git a/src/AppNavigator.tsx b/src/AppNavigator.tsx index 92cbc9154..9654cbea7 100644 --- a/src/AppNavigator.tsx +++ b/src/AppNavigator.tsx @@ -23,7 +23,6 @@ import { DAppAccessScreen } from 'screens/Settings/Security/DAppAccess'; import { DAppAccessDetailScreen } from 'screens/Settings/Security/DAppAccess/DAppAccessDetailScreen'; import { Languages } from 'screens/Settings/Languages'; import { Security } from 'screens/Settings/Security'; -import { PinCodeScreen } from 'screens/Settings/Security/PinCodeScreen'; import { AccountExport } from 'screens/Account/AccountExport'; import { CustomTokenSetting } from 'screens/Tokens'; import { NetworkConfig } from 'screens/Settings/NetworkConfig'; @@ -68,7 +67,7 @@ import { ConnectionList } from 'screens/Settings/WalletConnect/ConnectionList'; import { ConnectWalletConnect } from 'screens/Settings/WalletConnect/ConnectWalletConnect'; import { ConnectionDetail } from 'screens/Settings/WalletConnect/ConnectionDetail'; import useAppLock from 'hooks/useAppLock'; -import { LockScreen } from 'screens/LockScreen'; +import LoginScreen from 'screens/MasterPassword/Login'; import { STATUS_BAR_LIGHT_CONTENT } from 'styles/sharedStyles'; import { UnlockModal } from 'components/common/Modal/UnlockModal'; import { AppModalContext } from 'providers/AppModalContext'; @@ -143,6 +142,7 @@ const AppNavigator = ({ isAppReady }: Props) => { const { hasConfirmations } = useSelector((state: RootState) => state.requestState); const { accounts, hasMasterPassword } = useSelector((state: RootState) => state.accountState); const { isLocked } = useAppLock(); + const [isNavigationReady, setNavigationReady] = useState(false); const appModalContext = useContext(AppModalContext); const needMigrate = useMemo( @@ -179,7 +179,6 @@ const AppNavigator = ({ isAppReady }: Props) => { return () => { amount = false; }; - // eslint-disable-next-line react-hooks/exhaustive-deps }, [hasConfirmations, navigationRef, currentRoute]); useEffect(() => { @@ -195,16 +194,12 @@ const AppNavigator = ({ isAppReady }: Props) => { }, [currentRoute, hasMasterPassword, navigationRef, needMigrate]); useEffect(() => { - let amount = true; - if (isLocked && amount) { + if (isLocked && !!accounts.length && isNavigationReady) { appModalContext.hideConfirmModal(); - setTimeout(() => navigationRef.current?.navigate('Login'), 500); + setTimeout(() => navigationRef.current?.navigate('Login'), 300); } - return () => { - amount = false; - }; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isLocked, navigationRef]); + }, [isLocked, isNavigationReady, accounts]); useEffect(() => { if (isEmptyAccounts) { @@ -215,8 +210,17 @@ const AppNavigator = ({ isAppReady }: Props) => { } }, [isEmptyAccounts, navigationRef]); + const onNavigationReady = () => { + setNavigationReady(true); + }; + return ( - + { - { component={Confirmations} options={{ gestureEnabled: false, animationDuration: 100 }} /> - - {} + {!!accounts.length && } + )} diff --git a/src/AppNew.tsx b/src/AppNew.tsx index 4eab99e7f..d76d39daf 100644 --- a/src/AppNew.tsx +++ b/src/AppNew.tsx @@ -5,7 +5,7 @@ import { QrSignerContextProvider } from 'providers/QrSignerContext'; import { ScannerContextProvider } from 'providers/ScannerContext'; import { SigningContextProvider } from 'providers/SigningContext'; import React, { useEffect } from 'react'; -import { AppState, Platform, StatusBar, StyleProp, View } from 'react-native'; +import { AppState, StatusBar, StyleProp, View } from 'react-native'; import { ThemeContext } from 'providers/contexts'; import { THEME_PRESET } from 'styles/themes'; import { ToastProvider } from 'react-native-toast-notifications'; @@ -20,9 +20,8 @@ import { LoadingScreen } from 'screens/LoadingScreen'; import { ColorMap } from 'styles/color'; import { AutoLockState } from 'utils/autoLock'; import useStoreBackgroundService from 'hooks/store/useStoreBackgroundService'; -import { HIDE_MODAL_DURATION, TOAST_DURATION } from 'constants/index'; +import { TOAST_DURATION } from 'constants/index'; import AppNavigator from './AppNavigator'; -import { keyringLock } from 'messaging/index'; import { updateShowZeroBalanceState } from 'stores/utils'; import { setBuildNumber } from './stores/AppVersion'; import { getBuildNumber } from 'react-native-device-info'; @@ -31,6 +30,9 @@ import { CustomToast } from 'components/design-system-ui/toast'; import { PortalProvider } from '@gorhom/portal'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { SafeAreaProvider } from 'react-native-safe-area-context'; +import { LockTimeout } from 'stores/types'; +import { keyringLock } from './messaging'; +import { updateAutoLockTime } from 'stores/MobileSettings'; const layerScreenStyle: StyleProp = { top: 0, @@ -42,54 +44,65 @@ const layerScreenStyle: StyleProp = { zIndex: 10, }; -AutoLockState.isPreventAutoLock = false; +const gestureRootStyle: StyleProp = { + position: 'absolute', + top: 0, + left: 0, + bottom: 0, + right: 0, + width: '100%', + height: '100%', + zIndex: 9999, +}; + +// AutoLockState.isPreventAutoLock = false; const autoLockParams: { - pinCodeEnabled: boolean; - faceIdEnabled: boolean; - autoLockTime?: number; + hasMasterPassword: boolean; + isUseBiometric: boolean; + timeAutoLock?: number; lock: () => void; isPreventLock: boolean; + isMasterPasswordLocked: boolean; } = { - pinCodeEnabled: false, - faceIdEnabled: false, - isPreventLock: false, - autoLockTime: undefined, + hasMasterPassword: false, + isUseBiometric: false, + timeAutoLock: undefined, lock: () => {}, + isPreventLock: false, + isMasterPasswordLocked: false, }; -let timeout: NodeJS.Timeout | undefined; +// let timeout: NodeJS.Timeout | undefined; let lockWhenActive = false; AppState.addEventListener('change', (state: string) => { - const { pinCodeEnabled, faceIdEnabled, autoLockTime, lock, isPreventLock } = autoLockParams; - - if (state === 'background' && !isPreventLock) { - keyringLock().catch((e: Error) => console.log(e)); - } + const { isUseBiometric, timeAutoLock, lock, isMasterPasswordLocked } = autoLockParams; - if (!pinCodeEnabled || autoLockTime === undefined) { + if (timeAutoLock === undefined) { return; } if (state === 'background') { - timeout = setTimeout(() => { - if (AutoLockState.isPreventAutoLock) { - return; - } - if (faceIdEnabled) { - lockWhenActive = true; - } else { - lockWhenActive = false; - Platform.OS === 'android' ? setTimeout(() => lock(), HIDE_MODAL_DURATION) : lock(); + if (timeAutoLock === LockTimeout.ALWAYS) { + // Always lock incase always require + keyringLock().catch((e: Error) => console.log(e)); + } + if (AutoLockState.isPreventAutoLock) { + return; + } + if (isUseBiometric) { + lockWhenActive = true; + } else { + lockWhenActive = false; + if (isMasterPasswordLocked) { + lock(); } - }, autoLockTime); + } } else if (state === 'active') { if (lockWhenActive) { - if (!AutoLockState.isPreventAutoLock) { - Platform.OS === 'android' ? setTimeout(() => lock(), HIDE_MODAL_DURATION) : lock(); + if (isMasterPasswordLocked) { + lock(); } lockWhenActive = false; } - timeout && clearTimeout(timeout); - timeout = undefined; } }); @@ -100,52 +113,56 @@ export const AppNew = () => { const theme = isDarkMode ? THEME_PRESET.dark : THEME_PRESET.light; StatusBar.setBarStyle(isDarkMode ? 'light-content' : 'dark-content'); - const { pinCodeEnabled, faceIdEnabled, autoLockTime, isPreventLock } = useSelector( - (state: RootState) => state.mobileSettings, - ); - const { hasMasterPassword } = useSelector((state: RootState) => state.accountState); + const { isUseBiometric, timeAutoLock, isPreventLock } = useSelector((state: RootState) => state.mobileSettings); + const { hasMasterPassword, isLocked } = useSelector((state: RootState) => state.accountState); const { buildNumber } = useSelector((state: RootState) => state.appVersion); - const { lock } = useAppLock(); + const { lock, unlockApp } = useAppLock(); const dispatch = useDispatch(); - const isCryptoReady = useCryptoReady(); const isI18nReady = useSetupI18n().isI18nReady; useStoreBackgroundService(); // Enable lock screen on the start app useEffect(() => { - if (!firstTimeCheckPincode && pinCodeEnabled) { + if (!firstTimeCheckPincode && isLocked) { lock(); } + if (!isLocked) { + unlockApp(); + } firstTimeCheckPincode = true; - }, [lock, pinCodeEnabled]); + autoLockParams.isMasterPasswordLocked = isLocked; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isLocked]); useEffect(() => { autoLockParams.lock = lock; - autoLockParams.autoLockTime = autoLockTime; - autoLockParams.pinCodeEnabled = pinCodeEnabled; - autoLockParams.faceIdEnabled = faceIdEnabled; + autoLockParams.timeAutoLock = timeAutoLock; + autoLockParams.hasMasterPassword = hasMasterPassword; + autoLockParams.isUseBiometric = isUseBiometric; autoLockParams.isPreventLock = isPreventLock; - }, [autoLockTime, faceIdEnabled, isPreventLock, lock, pinCodeEnabled]); + }, [timeAutoLock, isUseBiometric, isPreventLock, lock, hasMasterPassword]); const isRequiredStoresReady = true; + // When update from v1.0.15, time auto lock could be wrong. We can remove this effect later + useEffect(() => { + if (!Object.values(LockTimeout).includes(timeAutoLock)) { + dispatch(updateAutoLockTime(LockTimeout._15MINUTE)); + } + }, [dispatch, timeAutoLock]); + useEffect(() => { setTimeout(() => { SplashScreen.hide(); }, 100); - }, []); - useEffect(() => { if (buildNumber === 1) { // Set default value on the first time install updateShowZeroBalanceState(false); const buildNumberInt = parseInt(getBuildNumber(), 10); dispatch(setBuildNumber(buildNumberInt)); } - if (hasMasterPassword) { - keyringLock().catch((e: Error) => console.log(e)); - } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -170,17 +187,7 @@ export const AppNew = () => { - + diff --git a/src/assets/fingerprint-simple.svg b/src/assets/fingerprint-simple.svg new file mode 100644 index 000000000..ea31b49fd --- /dev/null +++ b/src/assets/fingerprint-simple.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/assets/index.ts b/src/assets/index.ts index 7afe77a33..8bf126a0f 100644 --- a/src/assets/index.ts +++ b/src/assets/index.ts @@ -8,13 +8,16 @@ const CheckBoxFilledIcon = React.lazy(() => import('./checkbox-filled.svg')); const NftIcon = React.lazy(() => import('./logo-nft.svg')); const Logo = React.lazy(() => import('./subwallet-logo.svg')); const LogoGradient = React.lazy(() => import('./subwallet-logo-gradient.svg')); +const SubwalletStyled = React.lazy(() => import('./subwallet-styled.svg')); const MenuBarLogo = React.lazy(() => import('./menu-bar.svg')); const IcHalfSquare = React.lazy(() => import('./ic-half-square.svg')); const WalletConnect = React.lazy(() => import('./wallet-connect.svg')); +const Fingerprint = React.lazy(() => import('./fingerprint-simple.svg')); export const SVGImages = { Logo, LogoGradient, + SubwalletStyled, CheckBoxIcon, CheckBoxFilledIcon, NftIcon, @@ -23,6 +26,7 @@ export const SVGImages = { MenuBarLogo, IcHalfSquare, WalletConnect, + Fingerprint, }; export const Images = { diff --git a/src/assets/subwallet-styled.svg b/src/assets/subwallet-styled.svg new file mode 100644 index 000000000..45aa0178b --- /dev/null +++ b/src/assets/subwallet-styled.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/common/Field/Password/InlinePassword.tsx b/src/components/common/Field/Password/InlinePassword.tsx new file mode 100644 index 000000000..3fd4f9f05 --- /dev/null +++ b/src/components/common/Field/Password/InlinePassword.tsx @@ -0,0 +1,113 @@ +import React, { forwardRef, useMemo, useState } from 'react'; +import { TextInput, View, ViewStyle } from 'react-native'; +import { DisabledStyle } from 'styles/sharedStyles'; +import { FieldBaseProps } from 'components/Field/Base'; +import { Warning } from 'components/Warning'; +import { useSubWalletTheme } from 'hooks/useSubWalletTheme'; +import { Button, Icon, Input } from 'components/design-system-ui'; +import { Eye, EyeSlash, Key } from 'phosphor-react-native'; +import createStyles from './styles'; + +interface Props extends FieldBaseProps { + onChangeText?: (text: string) => void; + onEndEditing?: () => void; + onBlur?: () => void; + errorMessages?: string[]; + isBusy?: boolean; + autoFocus?: boolean; + onSubmitField?: () => void; + defaultValue?: string; + showEyeButton?: boolean; + placeholder?: string; + containerStyle?: ViewStyle; + disabled?: boolean; +} + +const InlinePassword = forwardRef((passwordFieldProps: Props, ref: React.Ref) => { + const { + defaultValue, + onChangeText, + onEndEditing, + onBlur, + errorMessages, + isBusy, + autoFocus, + onSubmitField, + showEyeButton = true, + placeholder, + containerStyle, + disabled, + } = passwordFieldProps; + const [isShowPassword, setShowPassword] = useState(false); + const [isFocus, setFocus] = useState(false); + const theme = useSubWalletTheme().swThemes; + const styles = useMemo( + () => createStyles(theme, !(errorMessages && errorMessages.length), undefined, isFocus), + [theme, errorMessages, isFocus], + ); + + const onInputFocus = () => { + setFocus(true); + }; + const onInputBlur = () => { + onBlur && onBlur(); + setFocus(false); + }; + + return ( + <> + + } + leftPartStyle={styles.leftInputStyle} + inputStyle={styles.textInput} + rightPart={ + showEyeButton && + (isShowPassword ? ( + + {isUseBiometric && ( + + )} {!isKeyboardVisible && } @@ -127,4 +220,4 @@ export const UnlockModal = () => { ); -}; +}); diff --git a/src/components/common/Modal/UnlockModal/style/index.ts b/src/components/common/Modal/UnlockModal/style/index.ts index 0154ba95d..602998448 100644 --- a/src/components/common/Modal/UnlockModal/style/index.ts +++ b/src/components/common/Modal/UnlockModal/style/index.ts @@ -3,15 +3,18 @@ import { ThemeTypes } from 'styles/themes'; import { FontSemiBold } from 'styles/sharedStyles'; export interface ComponentStyle { + root: ViewStyle; footer: ViewStyle; wrapper: ViewStyle; separator: ViewStyle; header: TextStyle; container: ViewStyle; + flex1: ViewStyle; } export default (theme: ThemeTypes) => { return StyleSheet.create({ + root: { flex: 1, flexDirection: 'column', justifyContent: 'flex-end' }, container: { width: '100%', backgroundColor: theme.colorBgDefault, @@ -42,5 +45,6 @@ export default (theme: ThemeTypes) => { textAlign: 'center', marginBottom: theme.margin, }, + flex1: { flex: 1 }, }); }; diff --git a/src/components/design-system-ui/modal/ActionHeader.tsx b/src/components/design-system-ui/modal/ActionHeader.tsx new file mode 100644 index 000000000..cf952a38b --- /dev/null +++ b/src/components/design-system-ui/modal/ActionHeader.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { View, TouchableOpacity } from 'react-native'; +import Typography from '../typography'; +import { useSubWalletTheme } from 'hooks/useSubWalletTheme'; +import ModalStyle from './style'; + +interface ActionHeaderProps { + title: string; + renderLeftAction?: React.ReactNode; + renderRightAction?: React.ReactNode; + onPressLeft?: () => void; + onPressRight?: () => void; +} +const ActionHeader: React.FC = ({ + title, + renderLeftAction, + renderRightAction, + onPressLeft, + onPressRight, +}) => { + const theme = useSubWalletTheme().swThemes; + const _styles = ModalStyle(theme); + return ( + + + {renderLeftAction} + + + {title} + + + {renderRightAction} + + + ); +}; + +export default ActionHeader; diff --git a/src/components/design-system-ui/modal/ModalBaseV2.tsx b/src/components/design-system-ui/modal/ModalBaseV2.tsx index 5a39282d9..30e88653c 100644 --- a/src/components/design-system-ui/modal/ModalBaseV2.tsx +++ b/src/components/design-system-ui/modal/ModalBaseV2.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useImperativeHandle, useState } from 'react'; -import { Dimensions, StyleProp, TouchableOpacity, View, ViewStyle } from 'react-native'; +import { DeviceEventEmitter, Dimensions, StyleProp, TouchableOpacity, View, ViewStyle } from 'react-native'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; import ModalStyles from './styleV2'; @@ -27,6 +27,8 @@ export type SWModalRefProps = { close: () => void; }; +export const FORCE_HIDDEN_EVENT = 'modalV2ForceHidden'; + const ModalBaseV2 = React.forwardRef( ( { @@ -48,6 +50,16 @@ const ModalBaseV2 = React.forwardRef( const _styles = ModalStyles(theme, level); const { numberOfConfirmations } = useConfirmationsInfo(); const [isForcedHidden, setForcedHidden] = useState(false); + + useEffect(() => { + const hiddenEvent = DeviceEventEmitter.addListener(FORCE_HIDDEN_EVENT, (isHidden: boolean) => { + setForcedHidden(isHidden); + }); + return () => { + hiddenEvent.remove(); + }; + }, []); + useEffect(() => { if (isUseForceHidden && !!numberOfConfirmations) { setForcedHidden(true); diff --git a/src/components/design-system-ui/modal/SwModal.tsx b/src/components/design-system-ui/modal/SwModal.tsx index 488fb7208..40a6b8513 100644 --- a/src/components/design-system-ui/modal/SwModal.tsx +++ b/src/components/design-system-ui/modal/SwModal.tsx @@ -27,6 +27,7 @@ export interface SWModalProps { modalBaseV2Ref?: React.RefObject; level?: number; isUseSafeAreaView?: boolean; + renderHeader?: React.ReactNode; } const getSubWalletModalContainerStyle = (isFullHeight: boolean): StyleProp => { @@ -77,6 +78,7 @@ const SwModal = React.forwardRef( modalBaseV2Ref, level, isUseSafeAreaView = true, + renderHeader, }, ref, ) => { @@ -178,7 +180,7 @@ const SwModal = React.forwardRef( contentContainerStyle, ]}> - {renderTitle()} + {renderHeader ? renderHeader : renderTitle()} {children} diff --git a/src/components/design-system-ui/modal/index.tsx b/src/components/design-system-ui/modal/index.tsx index 86d6e7aa7..7a03e92f4 100644 --- a/src/components/design-system-ui/modal/index.tsx +++ b/src/components/design-system-ui/modal/index.tsx @@ -1,9 +1,11 @@ import SWModal from './SwModal'; +import ActionHeader from './ActionHeader'; export type { SWModalProps as SWModalProps } from './SwModal'; const Modal = { SWModal, + ActionHeader, }; export default Modal; diff --git a/src/components/design-system-ui/modal/style/index.ts b/src/components/design-system-ui/modal/style/index.ts index 999b6d474..e918d3cc5 100644 --- a/src/components/design-system-ui/modal/style/index.ts +++ b/src/components/design-system-ui/modal/style/index.ts @@ -7,6 +7,9 @@ export interface ModalStyle { footerModalStyle: ViewStyle; deleteModalConfirmationStyle: TextStyle; deleteModalMessageTextStyle: TextStyle; + actionWrapper: ViewStyle; + actionContainer: ViewStyle; + headerTitle: TextStyle; } export default (theme: ThemeTypes) => @@ -28,4 +31,14 @@ export default (theme: ThemeTypes) => ...FontMedium, textAlign: 'center', }, + // Action Header + actionContainer: { + width: '100%', + marginBottom: 16, + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + actionWrapper: { width: 30 }, + headerTitle: { color: theme.colorTextLight1 }, }); diff --git a/src/hooks/modal/useUnlockModal.ts b/src/hooks/modal/useUnlockModal.ts index 0f4209935..38febb0f7 100644 --- a/src/hooks/modal/useUnlockModal.ts +++ b/src/hooks/modal/useUnlockModal.ts @@ -9,13 +9,14 @@ import { RootStackParamList } from 'routes/index'; interface Result { onPress: (onComplete: VoidFunction) => () => Promise | undefined; - onPasswordComplete: VoidFunction; - onHideModal: VoidFunction; + onHideModal: () => void; } const useUnlockModal = ( navigation: NativeStackNavigationProp, setLoading?: (arg: boolean) => void, + onUnlockComplete?: (arg: string) => void, + onCloseModal?: () => void, ): Result => { const { isLocked, hasMasterPassword } = useSelector((state: RootState) => state.accountState); const onCompleteRef = useRef(noop); @@ -26,8 +27,10 @@ const useUnlockModal = ( useEffect(() => { DeviceEventEmitter.addListener('unlockModal', data => { if (data.type === 'onComplete') { + !!onUnlockComplete && onUnlockComplete(data.password); onPasswordComplete(); } else { + !!onCloseModal && onCloseModal(); onHideModal(); } }); @@ -48,7 +51,7 @@ const useUnlockModal = ( setTimeout(() => { onCompleteRef.current = onComplete; - if (hasMasterPassword && isLocked) { + if ((hasMasterPassword && isLocked) || !!onUnlockComplete) { navigation.navigate('UnlockModal'); promiseRef.current = new Promise((resolve, reject) => { resolveRef.current = resolve; @@ -64,7 +67,8 @@ const useUnlockModal = ( } }; }, - [hasMasterPassword, isLocked, navigation], + // eslint-disable-next-line react-hooks/exhaustive-deps + [isLocked], ); const onPasswordComplete = useCallback(() => { @@ -85,7 +89,6 @@ const useUnlockModal = ( return { onPress, - onPasswordComplete, onHideModal, }; }; diff --git a/src/hooks/useAppLock.ts b/src/hooks/useAppLock.ts index ad5c1b43e..b49984e98 100644 --- a/src/hooks/useAppLock.ts +++ b/src/hooks/useAppLock.ts @@ -1,33 +1,29 @@ import { useDispatch, useSelector } from 'react-redux'; import { RootState } from 'stores/index'; import { useCallback } from 'react'; -import bcrypt from 'react-native-bcrypt'; import { updateLockState } from 'stores/AppState'; -import { updateFaceIdEnable, updatePinCode, updatePinCodeEnable } from 'stores/MobileSettings'; +import { resetBrowserSetting } from 'stores/Browser'; +import { updateUseBiometric } from 'stores/MobileSettings'; export interface UseAppLockOptions { isLocked: boolean; - unlock: (code: string) => boolean; - unlockWithBiometric: () => void; + unlock: () => void; + unlockApp: () => void; lock: () => void; resetPinCode: () => void; } export default function useAppLock(): UseAppLockOptions { const isLocked = useSelector((state: RootState) => state.appState.isLocked); - const { pinCode } = useSelector((state: RootState) => state.mobileSettings); const dispatch = useDispatch(); - const unlock = useCallback( - (code: string) => { - const compareRs = bcrypt.compareSync(code, pinCode); - dispatch(updateLockState(!compareRs)); - return compareRs; - }, - [dispatch, pinCode], - ); + const unlock = useCallback(() => { + // const compareRs = bcrypt.compareSync(code, pinCode); + // dispatch(updateLockState(!compareRs)); + // return compareRs; + }, []); - const unlockWithBiometric = useCallback(() => { + const unlockApp = useCallback(() => { dispatch(updateLockState(false)); }, [dispatch]); @@ -36,11 +32,10 @@ export default function useAppLock(): UseAppLockOptions { }, [dispatch]); const resetPinCode = useCallback(() => { - dispatch(updatePinCode('')); dispatch(updateLockState(false)); - dispatch(updatePinCodeEnable(false)); - dispatch(updateFaceIdEnable(false)); + dispatch(updateUseBiometric(false)); + dispatch(resetBrowserSetting()); }, [dispatch]); - return { isLocked, unlock, lock, unlockWithBiometric, resetPinCode }; + return { isLocked, unlock, lock, resetPinCode, unlockApp }; } diff --git a/src/messaging/index.ts b/src/messaging/index.ts index d43173f06..de0d79d49 100644 --- a/src/messaging/index.ts +++ b/src/messaging/index.ts @@ -633,6 +633,10 @@ export async function approveSignPasswordV2(request: RequestSigningApprovePasswo return sendMessage('pri(signing.approve.passwordV2)', request); } +export async function saveAutoLockTime(value: number): Promise { + return sendMessage('pri(settings.saveAutoLockTime)', { autoLockTime: value }); +} + export async function approveSignSignature(id: string, signature: HexString): Promise { return sendMessage('pri(signing.approve.signature)', { id, signature }); } diff --git a/src/routes/index.ts b/src/routes/index.ts index 1b10b7201..175f51c44 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -50,7 +50,6 @@ export type RootStackParamList = { Drawer: NavigatorScreenParams; Languages: undefined; Security: undefined; - PinCode: { screen: 'NewPinCode' | 'ChangePinCode' | 'TurnoffPinCode' }; AccountExport: { address: string }; ExportJson: { address: string }; BrowserHome?: NavigatorScreenParams | undefined; @@ -95,7 +94,6 @@ export type RootRouteProps = NavigationProps['route']; export type CreateAccountProps = NativeStackScreenProps; export type CreatePasswordProps = NativeStackScreenProps; export type ImportSecretPhraseProps = NativeStackScreenProps; -export type PinCodeProps = NativeStackScreenProps; export type AccountsScreenProps = NativeStackScreenProps; export type SendFundProps = NativeStackScreenProps; export type EditAccountProps = NativeStackScreenProps; diff --git a/src/screens/Home/Crypto/ServiceModal.tsx b/src/screens/Home/Crypto/ServiceModal.tsx index 5f9d313db..8bf5df72c 100644 --- a/src/screens/Home/Crypto/ServiceModal.tsx +++ b/src/screens/Home/Crypto/ServiceModal.tsx @@ -8,7 +8,6 @@ import { RootState } from 'stores/index'; import { InAppBrowser } from 'react-native-inappbrowser-reborn'; import { ServiceSelectItem } from 'components/ServiceSelectItem'; import { HIDE_MODAL_DURATION } from 'constants/index'; -import useAppLock from 'hooks/useAppLock'; import { PREDEFINED_TRANSAK_TOKEN, PREDEFINED_TRANSAK_TOKEN_BY_SLUG } from '../../../predefined/transak'; import { _getChainSubstrateAddressPrefix } from '@subwallet/extension-base/services/chain-service/utils'; import { ImageLogosMap } from 'assets/logo'; @@ -77,7 +76,7 @@ export const ServiceModal = ({ ? address : reformatAddress(address, networkPrefix === undefined ? -1 : networkPrefix); }, [token, address, networkPrefix]); - const { isLocked } = useAppLock(); + const { isLocked } = useSelector((state: RootState) => state.accountState); const url = useMemo((): string => { const host = HOST.PRODUCTION; diff --git a/src/screens/Home/index.tsx b/src/screens/Home/index.tsx index 4c8a9471e..e73c8f162 100644 --- a/src/screens/Home/index.tsx +++ b/src/screens/Home/index.tsx @@ -22,7 +22,6 @@ import { useDispatch, useSelector } from 'react-redux'; import { RootState } from 'stores/index'; import { ActivityIndicator } from 'components/design-system-ui'; import { useSubWalletTheme } from 'hooks/useSubWalletTheme'; -import useAppLock from 'hooks/useAppLock'; import { createDrawerNavigator, DrawerContentComponentProps } from '@react-navigation/drawer'; import { WrapperParamList } from 'routes/wrapper'; import { Settings } from 'screens/Settings'; @@ -179,8 +178,7 @@ interface Props { } export const Home = ({ navigation }: Props) => { const isEmptyAccounts = useCheckEmptyAccounts(); - const { hasMasterPassword, isReady } = useSelector((state: RootState) => state.accountState); - const { isLocked } = useAppLock(); + const { hasMasterPassword, isReady, isLocked } = useSelector((state: RootState) => state.accountState); const [isLoading, setLoading] = useState(true); const isFirstOpen = useRef(true); const toast = useToast(); diff --git a/src/screens/LockScreen.tsx b/src/screens/LockScreen.tsx deleted file mode 100644 index 9b2e9d137..000000000 --- a/src/screens/LockScreen.tsx +++ /dev/null @@ -1,186 +0,0 @@ -import React, { Suspense, useCallback, useEffect, useState } from 'react'; -import { ImageBackground, SafeAreaView, View } from 'react-native'; -import Text from 'components/Text'; -import { FontMedium, FontSemiBold, sharedStyles } from 'styles/sharedStyles'; -import { PinCodeField } from 'components/PinCodeField'; -import { ColorMap } from 'styles/color'; -import i18n from 'utils/i18n/i18n'; -import { useBlurOnFulfill } from 'react-native-confirmation-code-field'; -import { CELL_COUNT } from 'constants/index'; -import useAppLock from 'hooks/useAppLock'; -import TouchID from 'react-native-touch-id'; -import { useSelector } from 'react-redux'; -import { RootState } from 'stores/index'; -import { Images, SVGImages } from 'assets/index'; -import { useSubWalletTheme } from 'hooks/useSubWalletTheme'; -import { Button, WarningText } from 'components/design-system-ui'; -import { resetWallet } from 'messaging/index'; -import { useToast } from 'react-native-toast-notifications'; -import { ForgotPasswordModal } from 'components/common/ForgotPasswordModal'; -import { useNavigation } from '@react-navigation/native'; -import { RootNavigationProps } from 'routes/index'; - -const optionalConfigObject = { - title: 'Authentication Required', // Android - imageColor: '#e00606', // Android - imageErrorColor: '#ff0000', // Android - sensorDescription: 'Touch sensor', // Android - sensorErrorDescription: 'Failed', // Android - cancelText: 'Cancel', // Android - fallbackLabel: 'Enter Password', // iOS (if empty, then label is hidden) - unifiedErrors: false, // use unified error messages (default false) - passcodeFallback: false, // iOS - allows the device to fall back to using the passcode, if faceid/touch is not available. this does not mean that if touchid/faceid fails the first few times it will revert to passcode, rather that if the former are not enrolled, then it will use the passcode. -}; - -export const LockScreen = () => { - const theme = useSubWalletTheme().swThemes; - const { unlock, resetPinCode } = useAppLock(); - const faceIdEnabled = useSelector((state: RootState) => state.mobileSettings.faceIdEnabled); - const [value, setValue] = useState(''); - const [modalVisible, setModalVisible] = useState(false); - const [error, setError] = useState(''); - const [authMethod, setAuthMethod] = useState<'biometric' | 'pinCode'>(faceIdEnabled ? 'biometric' : 'pinCode'); - const ref = useBlurOnFulfill({ value, cellCount: CELL_COUNT }); - const toast = useToast(); - const [resetAccLoading, setAccLoading] = useState(false); - const [eraseAllLoading, setEraseAllLoading] = useState(false); - const navigation = useNavigation(); - - const unlockWithBiometric = useAppLock().unlockWithBiometric; - - useEffect(() => { - const _authMethod = faceIdEnabled ? 'biometric' : 'pinCode'; - if (_authMethod === 'biometric') { - TouchID.isSupported() - .then(currentType => { - TouchID.authenticate(`Sign in with ${currentType}`, optionalConfigObject) - .then(() => { - unlockWithBiometric(); - navigation.canGoBack() ? navigation.goBack() : navigation.navigate('Home'); - }) - .catch(() => { - setAuthMethod('pinCode'); - }); - }) - .catch(() => setAuthMethod('pinCode')); - } - setAuthMethod(_authMethod); - }, [faceIdEnabled, navigation, unlockWithBiometric]); - - useEffect(() => { - if (value.length === 6) { - if (unlock(value)) { - setValue(''); - navigation.canGoBack() ? navigation.goBack() : navigation.navigate('Home'); - } else { - setValue(''); - setError(i18n.errorMessage.invalidPinCode); - ref.current?.focus(); - } - } - }, [navigation, ref, unlock, value]); - - const onReset = useCallback( - (resetAll: boolean) => { - return () => { - const _setLoading = resetAll ? setEraseAllLoading : setAccLoading; - _setLoading(true); - - setTimeout(() => { - _setLoading(false); - resetWallet({ - resetAll: resetAll, - }) - .then(rs => { - if (!rs.status) { - toast.show(rs.errors[0], { type: 'danger' }); - } - resetPinCode(); - navigation.reset({ - index: 0, - routes: [{ name: 'Home' }], - }); - }) - .catch((e: Error) => { - toast.show(e.message, { type: 'danger' }); - }) - .finally(() => { - _setLoading(false); - setModalVisible(false); - }); - }, 300); - }; - }, - [navigation, resetPinCode, toast], - ); - - return ( - - - - - - - - - - {i18n.welcomeScreen.welcomeBackTitle} - - {authMethod === 'pinCode' && ( - <> - - {i18n.common.enterPinToUnlock} - - - - )} - - {!!error && } - - - - setModalVisible(false)} - resetAccLoading={resetAccLoading} - eraseAllLoading={eraseAllLoading} - /> - - - - - ); -}; diff --git a/src/screens/MasterPassword/ChangeMasterPassword/index.tsx b/src/screens/MasterPassword/ChangeMasterPassword/index.tsx index 9fe3c6f04..4d7abb569 100644 --- a/src/screens/MasterPassword/ChangeMasterPassword/index.tsx +++ b/src/screens/MasterPassword/ChangeMasterPassword/index.tsx @@ -18,6 +18,9 @@ import useGoHome from 'hooks/screen/useGoHome'; import i18n from 'utils/i18n/i18n'; import AlertBox from 'components/design-system-ui/alert-box'; import { FontSemiBold } from 'styles/sharedStyles'; +import { useSelector } from 'react-redux'; +import { RootState } from 'stores/index'; +import { createKeychainPassword, resetKeychainPassword } from 'utils/account'; function checkValidateForm(isValidated: Record) { return isValidated.password && isValidated.repeatPassword; @@ -27,6 +30,7 @@ type PageStep = 'OldPassword' | 'NewPassword'; const ChangeMasterPassword = () => { const navigation = useNavigation(); + const { isUseBiometric } = useSelector((state: RootState) => state.mobileSettings); const theme = useSubWalletTheme().swThemes; const goHome = useGoHome(); const _style = ChangeMasterPasswordStyle(theme); @@ -62,6 +66,13 @@ const ChangeMasterPassword = () => { backToHome(goHome); }, [goHome]); + async function handleUpdateKeychain(password: string) { + if (isUseBiometric) { + await resetKeychainPassword(); + createKeychainPassword(password); + } + } + const onSubmit = () => { if (checkValidateForm(formState.isValidated)) { const password = formState.data.password; @@ -78,6 +89,7 @@ const ChangeMasterPassword = () => { if (!res.status) { setErrors(res.errors); } else { + handleUpdateKeychain(password); _backToHome(); } }) diff --git a/src/screens/MasterPassword/CreateMasterPassword/index.tsx b/src/screens/MasterPassword/CreateMasterPassword/index.tsx index 930d4f23e..0e3695801 100644 --- a/src/screens/MasterPassword/CreateMasterPassword/index.tsx +++ b/src/screens/MasterPassword/CreateMasterPassword/index.tsx @@ -15,6 +15,9 @@ import { KeypairType } from '@polkadot/util-crypto/types'; import useHandlerHardwareBackPress from 'hooks/screen/useHandlerHardwareBackPress'; import AlertBox from 'components/design-system-ui/alert-box'; import i18n from 'utils/i18n/i18n'; +import { RootState } from 'stores/index'; +import { useSelector } from 'react-redux'; +import { createKeychainPassword } from 'utils/account'; function checkValidateForm(isValidated: Record) { return isValidated.password && isValidated.repeatPassword; @@ -26,6 +29,7 @@ const CreateMasterPassword = ({ }, }: CreatePasswordProps) => { const navigation = useNavigation(); + const { isUseBiometric } = useSelector(({ mobileSettings }: RootState) => mobileSettings); const theme = useSubWalletTheme().swThemes; const _style = CreateMasterPasswordStyle(theme); const [isBusy, setIsBusy] = useState(false); @@ -79,6 +83,9 @@ const CreateMasterPassword = ({ } else { onComplete(); // TODO: complete + if (isUseBiometric) { + createKeychainPassword(password); + } } }) .catch(e => { diff --git a/src/screens/MasterPassword/Login/index.tsx b/src/screens/MasterPassword/Login/index.tsx index d5685e6be..4308be86e 100644 --- a/src/screens/MasterPassword/Login/index.tsx +++ b/src/screens/MasterPassword/Login/index.tsx @@ -1,32 +1,78 @@ -import { ContainerWithSubHeader } from 'components/ContainerWithSubHeader'; -import { Button, Icon } from 'components/design-system-ui'; -import { PasswordField } from 'components/Field/Password'; +import { Button, Typography } from 'components/design-system-ui'; import useFormControl from 'hooks/screen/useFormControl'; -import { useSubWalletTheme } from 'hooks/useSubWalletTheme'; -import { CheckCircle } from 'phosphor-react-native'; -import React, { useEffect, useMemo, useState } from 'react'; -import { View } from 'react-native'; -import { validatePassword } from 'screens/Shared/AccountNamePasswordCreation'; +import React, { Suspense, useCallback, useEffect, useMemo, useState } from 'react'; +import { + DeviceEventEmitter, + ImageBackground, + Keyboard, + KeyboardAvoidingView, + Platform, + StyleProp, + TouchableWithoutFeedback, + View, +} from 'react-native'; import i18n from 'utils/i18n/i18n'; -import { keyringUnlock } from 'messaging/index'; +import { keyringUnlock, resetWallet } from 'messaging/index'; +import { Images, SVGImages } from 'assets/index'; +import { InlinePassword } from 'components/common/Field/Password'; +import createStyles from './styles'; +import useAppLock from 'hooks/useAppLock'; +import { useDispatch, useSelector } from 'react-redux'; +import { RootState } from 'stores/index'; +import { SafeAreaView } from 'react-native-safe-area-context'; +import { ForgotPasswordModal } from 'components/common/ForgotPasswordModal'; +import { useToast } from 'react-native-toast-notifications'; +import useHandlerHardwareBackPress from 'hooks/screen/useHandlerHardwareBackPress'; +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import { RootStackParamList } from 'routes/index'; +import { createKeychainPassword, getKeychainPassword, getSupportedBiometryType } from 'utils/account'; +import { updateFaceIdEnable, updateUseBiometric } from 'stores/MobileSettings'; +import { TouchableOpacity } from 'react-native-gesture-handler'; +import { FORCE_HIDDEN_EVENT } from 'components/design-system-ui/modal/ModalBaseV2'; -type Props = {}; +interface LoginProps { + navigation: NativeStackNavigationProp; +} +type AuthMethod = 'biometric' | 'master-password'; -const Login: React.FC = (props: Props) => { - const {} = props; - const theme = useSubWalletTheme().swThemes; +const imageBackgroundStyle: StyleProp = { + flex: 1, + alignItems: 'center', + paddingHorizontal: 16, + paddingBottom: Platform.OS === 'ios' ? 56 : 20, + position: 'relative', + backgroundColor: 'black', +}; +// on Android, react navigation modal stacks doesn't in root level, it could be overlap +function forceCloseModalV2(isForceClose: boolean) { + if (Platform.OS === 'android') { + DeviceEventEmitter.emit(FORCE_HIDDEN_EVENT, isForceClose); + } +} + +const Login: React.FC = ({ navigation }) => { + const { faceIdEnabled, isUseBiometric } = useSelector((state: RootState) => state.mobileSettings); const [loading, setLoading] = useState(false); + const [modalVisible, setModalVisible] = useState(false); + const [resetAccLoading, setAccLoading] = useState(false); + const [eraseAllLoading, setEraseAllLoading] = useState(false); + const [isBiometricEnabled, setIsBiometricEnabled] = useState(isUseBiometric); + const dispatch = useDispatch(); + + const toast = useToast(); + const [authMethod, setAuthMethod] = useState(isUseBiometric ? 'biometric' : 'master-password'); + const styles = createStyles(); + const { unlockApp, resetPinCode } = useAppLock(); const formConfig = { password: { name: i18n.common.walletPassword, value: '', - validateFunc: validatePassword, - require: true, + require: false, }, }; + useHandlerHardwareBackPress(true); - const onSubmit = () => { - const password = formState.data.password; + const onUnlock = useCallback((password: string) => { setLoading(true); setTimeout(() => { keyringUnlock({ @@ -35,6 +81,24 @@ const Login: React.FC = (props: Props) => { .then(data => { if (!data.status) { onUpdateErrors('password')([i18n.errorMessage.invalidMasterPassword]); + return; + } + unlockApp(); + if (faceIdEnabled && !isUseBiometric) { + // Migrate use biometrics + createKeychainPassword(password) + .then(res => { + if (res) { + dispatch(updateFaceIdEnable(false)); + dispatch(updateUseBiometric(true)); + } else { + dispatch(updateUseBiometric(false)); + } + }) + .finally(() => { + forceCloseModalV2(false); + navigation.goBack(); + }); } }) .catch((e: Error) => { @@ -44,48 +108,166 @@ const Login: React.FC = (props: Props) => { setLoading(false); }); }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => forceCloseModalV2(true), []); + useEffect(() => { + if (!isUseBiometric) { + return; + } + if (Platform.OS === 'ios') { + // Because only iOS-Face ID is require permission, then we need to check permission's availbility + (async () => { + try { + const isBiometricAvailable = await getSupportedBiometryType(); + if (isBiometricAvailable) { + requestUnlockWithBiometric(); + } else { + setIsBiometricEnabled(false); + setAuthMethod('master-password'); + } + } catch (e) { + setAuthMethod('master-password'); + console.error(e); + } + })(); + return; + } + requestUnlockWithBiometric(); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + async function requestUnlockWithBiometric() { + try { + const password = await getKeychainPassword(); + if (password) { + onUnlock(password); + } + } catch (e) { + console.warn(e); + if (JSON.stringify(e).indexOf('Biometry is not available') !== -1) { + setIsBiometricEnabled(false); + setAuthMethod('master-password'); + } else { + setAuthMethod('master-password'); + } + } + } + + const onSubmit = () => { + const password = formState.data.password; + onUnlock(password); }; const { formState, onChangeValue, onSubmitField, focus, onUpdateErrors } = useFormControl(formConfig, { onSubmitForm: onSubmit, }); + useEffect(() => { + if (authMethod === 'master-password') { + focus('password')(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [authMethod]); + const isDisabled = useMemo(() => { return loading || !formState.data.password || formState.errors.password.length > 0; }, [formState.data.password, formState.errors.password.length, loading]); - useEffect(() => { - focus('password')(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + const onReset = useCallback( + (resetAll: boolean) => { + return () => { + const _setLoading = resetAll ? setEraseAllLoading : setAccLoading; + _setLoading(true); + + setTimeout(() => { + _setLoading(false); + resetWallet({ + resetAll: resetAll, + }) + .then(rs => { + if (!rs.status) { + toast.show(rs.errors[0], { type: 'danger' }); + } + }) + .catch((e: Error) => { + toast.show(e.message, { type: 'danger' }); + }) + .finally(() => { + _setLoading(false); + setModalVisible(false); + resetAll && resetPinCode(); + }); + }, 300); + }; + }, + [toast, resetPinCode], + ); + const onToggleModal = () => setModalVisible(state => !state); + + const dismissKeyboard = () => Keyboard.dismiss(); return ( - - onChangeValue('password')(value)} - errorMessages={formState.errors.password} - onSubmitField={onSubmitField('password')} - /> - - + {isUseBiometric && isBiometricEnabled && ( + + )} + + )} + + - } - onPress={onSubmit}> - {i18n.buttonTitles.apply} - - - + + + + ); }; diff --git a/src/screens/MasterPassword/Login/styles/index.ts b/src/screens/MasterPassword/Login/styles/index.ts index 926e5f830..2e09239e9 100644 --- a/src/screens/MasterPassword/Login/styles/index.ts +++ b/src/screens/MasterPassword/Login/styles/index.ts @@ -1,5 +1,27 @@ -import { StyleSheet } from 'react-native'; +import { useSubWalletTheme } from 'hooks/useSubWalletTheme'; +import { StyleSheet, TextStyle, ViewStyle } from 'react-native'; -export interface LoginStyle {} +export interface LoginStyle { + container: ViewStyle; + subLogo: ViewStyle; + subTitle: ViewStyle; + submitButton: ViewStyle; + forgotpasswordText: TextStyle; + forgotpasswordButton: ViewStyle; + fullscreen: ViewStyle; + fullWidth: ViewStyle; +} -export default () => StyleSheet.create({}); +export default () => { + const theme = useSubWalletTheme().swThemes; + return StyleSheet.create({ + container: { width: '100%', alignItems: 'center', paddingTop: 93 }, + subLogo: { paddingTop: 20, paddingBottom: 12 }, + subTitle: { marginBottom: 40, color: theme.colorTextLabel }, + submitButton: { width: '100%', marginTop: 8 }, + fullscreen: { width: '100%', height: '100%' }, + fullWidth: { width: '100%' }, + forgotpasswordText: { color: theme.colorTextDescription }, + forgotpasswordButton: { alignSelf: 'flex-end', height: 35, justifyContent: 'center' }, + }); +}; diff --git a/src/screens/Settings/Security/PinCode.tsx b/src/screens/Settings/Security/PinCode.tsx deleted file mode 100644 index 87e6fc246..000000000 --- a/src/screens/Settings/Security/PinCode.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React from 'react'; -import { StyleProp, View } from 'react-native'; -import { PinCodeField } from 'components/PinCodeField'; -import { useBlurOnFulfill } from 'react-native-confirmation-code-field'; -import { CELL_COUNT } from 'constants/index'; -import i18n from 'utils/i18n/i18n'; -import { Button } from 'components/design-system-ui'; - -const bottomAreaStyle: StyleProp = { - flexDirection: 'row', - width: '100%', - paddingHorizontal: 16, - paddingBottom: 18, - paddingTop: 118, -}; - -const cancelButtonStyle: StyleProp = { flex: 1, marginRight: 6 }; -const continueButtonStyle: StyleProp = { flex: 1, marginLeft: 6 }; -interface Props { - pinCode: string; - onChangePinCode: (text: string) => void; - onPressBack: () => void; - onPressContinue: () => void; - isPinCodeValid?: boolean; -} - -export const PinCode = ({ pinCode, onChangePinCode, onPressBack, onPressContinue, isPinCodeValid }: Props) => { - const ref = useBlurOnFulfill({ value: pinCode, cellCount: CELL_COUNT }); - return ( - <> - - - - - - - - - ); -}; diff --git a/src/screens/Settings/Security/PinCodeScreen.tsx b/src/screens/Settings/Security/PinCodeScreen.tsx deleted file mode 100644 index 05e8b1a81..000000000 --- a/src/screens/Settings/Security/PinCodeScreen.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import React, { useState } from 'react'; -import { ContainerWithSubHeader } from 'components/ContainerWithSubHeader'; -import { PinCode } from 'screens/Settings/Security/PinCode'; -import { updateFaceIdEnable, updatePinCode, updatePinCodeEnable } from 'stores/MobileSettings'; -import { useDispatch, useSelector } from 'react-redux'; -import { useNavigation } from '@react-navigation/native'; -import { PinCodeProps, RootNavigationProps } from 'routes/index'; -import { RootState } from 'stores/index'; -import bcrypt from 'react-native-bcrypt'; -import i18n from 'utils/i18n/i18n'; - -const ViewStep = { - VALIDATE_PIN_CODE: 1, - PIN_CODE: 2, - REPEAT_PIN_CODE: 3, -}; - -export const PinCodeScreen = ({ - route: { - params: { screen }, - }, -}: PinCodeProps) => { - const pinCode = useSelector((state: RootState) => state.mobileSettings.pinCode); - const navigation = useNavigation(); - const [currentViewStep, setCurrentViewStep] = useState( - screen === 'NewPinCode' ? ViewStep.PIN_CODE : ViewStep.VALIDATE_PIN_CODE, - ); - const [title, setTitle] = useState(screen ? i18n.common.pinCode : i18n.common.newPinCode); - const [validatePinCode, setValidatePinCode] = useState(''); - const [newPinCode, setNewPinCode] = useState(''); - const [repeatPinCode, setRepeatPinCode] = useState(''); - const dispatch = useDispatch(); - const onSavePinCode = () => { - const salt = bcrypt.genSaltSync(6); - const hash = bcrypt.hashSync(newPinCode, salt); - dispatch(updatePinCode(hash)); - dispatch(updatePinCodeEnable(true)); - navigation.navigate('Security'); - }; - - const onPressBack = () => { - if (currentViewStep === ViewStep.VALIDATE_PIN_CODE) { - navigation.navigate('Security'); - } else if (currentViewStep === ViewStep.PIN_CODE) { - navigation.navigate('Security'); - } else { - setCurrentViewStep(ViewStep.PIN_CODE); - setTitle(i18n.common.newPinCode); - setRepeatPinCode(''); - } - }; - - return ( - - <> - {currentViewStep === ViewStep.VALIDATE_PIN_CODE && ( - navigation.navigate('Security')} - onPressContinue={() => { - if (screen === 'TurnoffPinCode') { - dispatch(updatePinCodeEnable(false)); - dispatch(updateFaceIdEnable(false)); - dispatch(updatePinCode('')); - navigation.navigate('Security'); - } else { - setCurrentViewStep(ViewStep.PIN_CODE); - setTitle(i18n.common.newPinCode); - } - }} - pinCode={validatePinCode} - onChangePinCode={setValidatePinCode} - isPinCodeValid={ - validatePinCode.length === 6 ? !!pinCode && bcrypt.compareSync(validatePinCode, pinCode) : true - } - /> - )} - - {currentViewStep === ViewStep.PIN_CODE && ( - { - navigation.navigate('Security'); - }} - onPressContinue={() => { - setCurrentViewStep(ViewStep.REPEAT_PIN_CODE); - setTitle(i18n.common.repeatPinCode); - }} - isPinCodeValid={true} - /> - )} - - {currentViewStep === ViewStep.REPEAT_PIN_CODE && ( - { - setCurrentViewStep(ViewStep.PIN_CODE); - setTitle(i18n.common.newPinCode); - setRepeatPinCode(''); - }} - onPressContinue={onSavePinCode} - pinCode={repeatPinCode} - onChangePinCode={setRepeatPinCode} - isPinCodeValid={repeatPinCode.length === 6 ? newPinCode === repeatPinCode : true} - /> - )} - - - ); -}; diff --git a/src/screens/Settings/Security/index.tsx b/src/screens/Settings/Security/index.tsx index 415050bfb..e55c501eb 100644 --- a/src/screens/Settings/Security/index.tsx +++ b/src/screens/Settings/Security/index.tsx @@ -5,80 +5,98 @@ import { RootNavigationProps } from 'routes/index'; import { ToggleItem } from 'components/ToggleItem'; import { View } from 'react-native'; import { sharedStyles } from 'styles/sharedStyles'; -import { CaretRight, Globe, Key, Password, Scan, ShieldCheck } from 'phosphor-react-native'; +import { CaretRight, Globe, Key, Scan, ShieldCheck } from 'phosphor-react-native'; import { useDispatch, useSelector } from 'react-redux'; import { RootState } from 'stores/index'; -import { updateAutoLockTime, updateFaceIdEnable } from 'stores/MobileSettings'; +import { updateAutoLockTime, updateUseBiometric } from 'stores/MobileSettings'; import i18n from 'utils/i18n/i18n'; import { useSubWalletTheme } from 'hooks/useSubWalletTheme'; import { Icon, SelectItem, SwModal } from 'components/design-system-ui'; import { useToast } from 'react-native-toast-notifications'; import { SWModalRefProps } from 'components/design-system-ui/modal/ModalBaseV2'; +import useUnlockModal from 'hooks/modal/useUnlockModal'; +import { createKeychainPassword, getSupportedBiometryType, resetKeychainPassword } from 'utils/account'; +import { saveAutoLockTime } from 'messaging/index'; +import { requestFaceIDPermission } from 'utils/permission/biometric'; +import { LockTimeout } from 'stores/types'; export const Security = () => { const theme = useSubWalletTheme().swThemes; const toast = useToast(); - const { pinCode, pinCodeEnabled, autoLockTime, faceIdEnabled } = useSelector( - (state: RootState) => state.mobileSettings, - ); + const { timeAutoLock, isUseBiometric } = useSelector((state: RootState) => state.mobileSettings); const [iShowAutoLockModal, setIsShowAutoLockModal] = useState(false); const navigation = useNavigation(); const dispatch = useDispatch(); const modalRef = useRef(null); - const AUTO_LOCK_LIST: { text: string; value: number | undefined }[] = [ - { - text: i18n.settings.immediately, - value: 0, - }, - { - text: i18n.settings.ifLeftFor15Seconds, - value: 15 * 1000, - }, - { - text: i18n.settings.ifLeftFor30Seconds, - value: 30 * 1000, - }, + const AUTO_LOCK_LIST: { text: string; value: number }[] = [ + // { + // text: i18n.settings.alwaysRequire, + // value: LockTimeout.ALWAYS, + // }, { text: i18n.settings.ifLeftFor1Minute, - value: 60 * 1000, + value: LockTimeout._1MINUTE, }, { text: i18n.settings.ifLeftFor5Minutes, - value: 5 * 60 * 1000, + value: LockTimeout._5MINUTE, + }, + { + text: i18n.settings.ifLeftFor10Minutes, + value: LockTimeout._10MINUTE, }, { text: i18n.settings.ifLeftFor15Minutes, - value: 15 * 60 * 1000, + value: LockTimeout._15MINUTE, }, { text: i18n.settings.ifLeftFor30Minutes, - value: 30 * 60 * 1000, + value: LockTimeout._30MINUTE, }, { text: i18n.settings.ifLeftFor1Hour, - value: 60 * 60 * 1000, - }, - { - text: i18n.settings.whenCloseApp, - value: undefined, + value: LockTimeout._60MINUTE, }, + // { + // text: i18n.settings.neverRequire, + // value: LockTimeout.NEVER, + // }, ]; - const onValueChangePinCode = () => { - if (!pinCodeEnabled) { - navigation.navigate('PinCode', { screen: 'NewPinCode' }); - } else { - navigation.navigate('PinCode', { screen: 'TurnoffPinCode' }); - } + const onPasswordComplete = (password: string) => { + createKeychainPassword(password).then(res => { + if (res) { + dispatch(updateUseBiometric(true)); + } else { + dispatch(updateUseBiometric(false)); + } + }); }; + const { onPress: onPressSubmit } = useUnlockModal(navigation, () => {}, onPasswordComplete); const onValueChangeFaceId = () => { - dispatch(updateFaceIdEnable(!faceIdEnabled)); + if (isUseBiometric) { + dispatch(updateUseBiometric(false)); + resetKeychainPassword(); + } else { + (async () => { + const isBiometricEnabled = await getSupportedBiometryType(); + if (isBiometricEnabled) { + onPressSubmit(() => {})(); + return; + } + // if Face ID permission denied + const result = await requestFaceIDPermission(); + if (result) { + onPressSubmit(() => {})(); + } + })(); + } }; - const onChangeAutoLockTime = (value: number | undefined) => { - dispatch(updateAutoLockTime(value)); + const onChangeAutoLockTime = (value: number) => { + saveAutoLockTime(value).then(() => dispatch(updateAutoLockTime(value))); modalRef?.current?.close(); }; @@ -90,39 +108,16 @@ export const Security = () => { navigation.goBack(); }}> - - navigation.navigate('PinCode', { screen: 'ChangePinCode' })} - rightIcon={ - - } - disabled={!pinCode} - /> - { backgroundColor={theme['green-6']} label={i18n.settings.appLock} onPress={() => setIsShowAutoLockModal(true)} - rightIcon={ - - } - disabled={!pinCode} + rightIcon={} /> @@ -176,7 +164,7 @@ export const Security = () => { {AUTO_LOCK_LIST.map(item => ( onChangeAutoLockTime(item.value)} /> diff --git a/src/screens/Settings/index.tsx b/src/screens/Settings/index.tsx index c9270fcee..b0bbf44ba 100644 --- a/src/screens/Settings/index.tsx +++ b/src/screens/Settings/index.tsx @@ -24,8 +24,6 @@ import { } from 'phosphor-react-native'; import { FontMedium, FontSemiBold, sharedStyles } from 'styles/sharedStyles'; import { ColorMap } from 'styles/color'; -import { useSelector } from 'react-redux'; -import { RootState } from 'stores/index'; import { RootNavigationProps } from 'routes/index'; import i18n from 'utils/i18n/i18n'; import { @@ -73,7 +71,6 @@ type settingItemType = { export const Settings = ({ navigation: drawerNavigation }: DrawerContentComponentProps) => { const navigation = useNavigation(); const theme = useSubWalletTheme().swThemes; - const pinCodeEnabled = useSelector((state: RootState) => state.mobileSettings.pinCodeEnabled); const { lock } = useAppLock(); const [hiddenCount, setHiddenCount] = useState(0); @@ -285,17 +282,9 @@ export const Settings = ({ navigation: drawerNavigation }: DrawerContentComponen diff --git a/src/stores/Browser.ts b/src/stores/Browser.ts index 84accdd4d..db009cccc 100644 --- a/src/stores/Browser.ts +++ b/src/stores/Browser.ts @@ -117,7 +117,15 @@ const browserSlice = createSlice({ removeBookmark: (state, { payload }: PayloadAction) => { state.bookmarks = state.bookmarks.filter(t => t.url !== payload.url); }, + resetBrowserSetting: state => { + state.activeTab = null; + state.tabs = []; + state.whitelist = []; + state.history = []; + state.bookmarks = []; + }, }, }); +export const { resetBrowserSetting } = browserSlice.actions; export default browserSlice.reducer; diff --git a/src/stores/MobileSettings.ts b/src/stores/MobileSettings.ts index bbba694ac..6ea35f7fd 100644 --- a/src/stores/MobileSettings.ts +++ b/src/stores/MobileSettings.ts @@ -1,13 +1,13 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { MobileSettingsSlice } from 'stores/types'; +import { LockTimeout, MobileSettingsSlice } from 'stores/types'; const MOBILE_SETTINGS_STORE_DEFAULT: MobileSettingsSlice = { language: 'en', - pinCode: '', pinCodeEnabled: false, - faceIdEnabled: false, + faceIdEnabled: false, // deprecated + isUseBiometric: false, isPreventLock: false, - autoLockTime: 15 * 1000, + timeAutoLock: LockTimeout._15MINUTE, }; const mobileSettingsSlice = createSlice({ @@ -22,17 +22,14 @@ const mobileSettingsSlice = createSlice({ updateLanguage(state, action: PayloadAction) { state.language = action.payload; }, - updatePinCode(state, action: PayloadAction) { - state.pinCode = action.payload; - }, - updatePinCodeEnable(state, action: PayloadAction) { - state.pinCodeEnabled = action.payload; - }, updateFaceIdEnable(state, action: PayloadAction) { state.faceIdEnabled = action.payload; }, + updateUseBiometric(state, action: PayloadAction) { + state.isUseBiometric = action.payload; + }, updateAutoLockTime(state, action: PayloadAction) { - state.autoLockTime = action.payload; + state.timeAutoLock = action.payload; }, updatePreventLock(state, action: PayloadAction) { state.isPreventLock = action.payload; @@ -40,12 +37,6 @@ const mobileSettingsSlice = createSlice({ }, }); -export const { - updateLanguage, - updatePinCode, - updatePinCodeEnable, - updateFaceIdEnable, - updateAutoLockTime, - updatePreventLock, -} = mobileSettingsSlice.actions; +export const { updateLanguage, updateFaceIdEnable, updateUseBiometric, updateAutoLockTime, updatePreventLock } = + mobileSettingsSlice.actions; export default mobileSettingsSlice.reducer; diff --git a/src/stores/types.ts b/src/stores/types.ts index dca2dc75c..06b73c241 100644 --- a/src/stores/types.ts +++ b/src/stores/types.ts @@ -81,13 +81,24 @@ export type ConfirmationSlice = { export type MobileSettingsSlice = { language: string; - pinCode: string; pinCodeEnabled: boolean; - faceIdEnabled: boolean; - autoLockTime: number | undefined; + faceIdEnabled: boolean; // deprecated + isUseBiometric: boolean; + timeAutoLock: LockTimeout; isPreventLock: boolean; }; +export enum LockTimeout { + NEVER = -1, + ALWAYS = 0, + _1MINUTE = 1, + _5MINUTE = 5, + _10MINUTE = 10, + _15MINUTE = 15, + _30MINUTE = 30, + _60MINUTE = 60, +} + export type SiteInfo = { name: string; url: string; diff --git a/src/utils/account.ts b/src/utils/account.ts index 68f1b2aba..df634e305 100644 --- a/src/utils/account.ts +++ b/src/utils/account.ts @@ -11,6 +11,7 @@ import { AccountSignMode } from 'types/signer'; import { AccountAddressType } from 'types/index'; import { _ChainInfo } from '@subwallet/chain-list/types'; import { Recoded } from 'types/ui-types'; +import SInfo, { RNSensitiveInfoOptions } from 'react-native-sensitive-info'; export const findAccountByAddress = (accounts: AccountJson[], address?: string): AccountJson | null => { try { @@ -193,3 +194,54 @@ export const findContactByAddress = (contacts: AbstractAddressJson[], address?: return null; } }; + +// Keychain configuration +const keychainConfig: RNSensitiveInfoOptions = { + touchID: true, + showModal: true, + kSecAccessControl: 'kSecAccessControlBiometryCurrentSet', + sharedPreferencesName: 'swSharedPrefs', + keychainService: 'swKeychain', + kSecAttrAccessible: 'kSecAttrAccessibleWhenUnlocked', + kSecUseOperationPrompt: 'Unlock app using biometric', +}; +const username = 'sw-user'; +export const createKeychainPassword = async (password: string) => { + try { + await SInfo.setItem(username, password, keychainConfig); + return true; + } catch (e) { + console.warn('set keychain failed', e); + return false; + } +}; + +export const getKeychainPassword = async () => { + try { + const password = await SInfo.getItem(username, keychainConfig); + return password; + } catch (e) { + throw e; + } +}; + +export const resetKeychainPassword = async () => { + try { + // return await Keychain.resetGenericPassword(); + SInfo.deleteItem(username, keychainConfig); + return true; + } catch (e) { + console.warn('reset keychain failed:', e); + return false; + } +}; + +export const getSupportedBiometryType = async () => { + try { + const result = await SInfo.isSensorAvailable(); + return result; + } catch (e) { + console.warn('Get failed!'); + return null; + } +}; diff --git a/src/utils/i18n/en_US.ts b/src/utils/i18n/en_US.ts index 5d64715f6..59eaa7244 100644 --- a/src/utils/i18n/en_US.ts +++ b/src/utils/i18n/en_US.ts @@ -12,6 +12,7 @@ export const en = { cannotScanQRCodeWithoutPermission: 'SubWallet needs access to camera on your device to scan QR code for actions such as account creation, data verification or dApp connection.', goToSetting: 'Go to setting', + noFaceIdPermission: 'This app use Face ID to unlock password', scan: 'Scan', toSendFund: 'to send fund', toSendAsset: 'to send asset', @@ -393,6 +394,7 @@ export const en = { applyAccounts: (account: number) => `Apply ${account} accounts`, createOne: 'Create one', reload: 'Reload', + unlockWithBiometric: 'Unlock with your biometric', }, inputLabel: { selectAcc: 'Select account', @@ -954,10 +956,13 @@ export const en = { termOfService: 'Terms of service', webViewDebugger: 'Web view debugger', immediately: 'Immediately', + neverRequire: 'Never', + alwaysRequire: 'Always', ifLeftFor15Seconds: 'If left for 15 seconds', ifLeftFor30Seconds: 'If left for 30 seconds', ifLeftFor1Minute: 'If left for 1 minute', ifLeftFor5Minutes: 'If left for 5 minutes', + ifLeftFor10Minutes: 'If left for 10 minutes', ifLeftFor15Minutes: 'If left for 15 minutes', ifLeftFor30Minutes: 'If left for 30 minutes', ifLeftFor1Hour: 'If left for 1 hour', diff --git a/src/utils/i18n/vi_VN.ts b/src/utils/i18n/vi_VN.ts index 4422f6eec..06850267b 100644 --- a/src/utils/i18n/vi_VN.ts +++ b/src/utils/i18n/vi_VN.ts @@ -12,6 +12,7 @@ export const vi = { cannotScanQRCodeWithoutPermission: 'SubWallet cần sử dụng máy ảnh trên thiết bị của bạn để quét mã QR nhằm thực hiện các hành động như tạo tài khoản, xác thực dữ liệu hoặc kết nối dApp', goToSetting: 'Đi đến Cài đặt', + noFaceIdPermission: 'This app use Face ID to unlock password', scan: 'Quét', toSendFund: 'để gửi tài sản ', toSendAsset: 'để gửi tài sản', @@ -392,6 +393,7 @@ export const vi = { applyAccounts: (account: number) => `Kết nối ${account} tài khoản`, createOne: 'Tạo tài khoản', reload: 'Tải lại', + unlockWithBiometric: 'Mở khoá bằng sinh trắc học', }, inputLabel: { selectAcc: 'Chọn tài khoản', @@ -953,10 +955,13 @@ export const vi = { termOfService: 'Điều khoản dịch vụ', webViewDebugger: 'Trình gỡ lỗi web view', immediately: 'Ngay lập tức', + neverRequire: 'Never', + alwaysRequire: 'Always', ifLeftFor15Seconds: 'Sau 15 giây', ifLeftFor30Seconds: 'Sau 30 giây', ifLeftFor1Minute: 'Sau 1 phút', ifLeftFor5Minutes: 'Sau 5 phút', + ifLeftFor10Minutes: 'Sau 10 phút', ifLeftFor15Minutes: 'Sau 15 phút', ifLeftFor30Minutes: 'Sau 30 phút', ifLeftFor1Hour: 'Sau 1 giờ', diff --git a/src/utils/i18n/zh_CN.ts b/src/utils/i18n/zh_CN.ts index 9bb378e05..4874d6789 100644 --- a/src/utils/i18n/zh_CN.ts +++ b/src/utils/i18n/zh_CN.ts @@ -12,6 +12,7 @@ export const zh = { cannotScanQRCodeWithoutPermission: 'SubWallet 需要访问您设备上的摄像头来扫描二维码以执行帐户创建、数据验证或 dApp 连接等操作。', goToSetting: '前往设置', + noFaceIdPermission: 'This app use Face ID to unlock password', scan: '扫描', toSendFund: '以发送资金', toSendAsset: '以发送资产', @@ -388,6 +389,7 @@ export const zh = { applyAccounts: (account: number) => `应用${account}账户`, createOne: '创建', reload: '重新加载', + unlockWithBiometric: '生物识别解锁', }, inputLabel: { selectAcc: '选择账户', @@ -943,10 +945,13 @@ export const zh = { termOfService: '服务条款', webViewDebugger: '网页视图排查者', immediately: '立即', + neverRequire: 'Never', + alwaysRequire: 'Always', ifLeftFor15Seconds: '若离开15秒', ifLeftFor30Seconds: '若离开30秒', ifLeftFor1Minute: '若离开1分钟', ifLeftFor5Minutes: '若离开5分钟', + ifLeftFor10Minutes: '若离开10分钟', ifLeftFor15Minutes: '若离开15分钟', ifLeftFor30Minutes: '若离开30分钟', ifLeftFor1Hour: '若离开1小时', diff --git a/src/utils/permission/biometric.ts b/src/utils/permission/biometric.ts new file mode 100644 index 000000000..5590377fa --- /dev/null +++ b/src/utils/permission/biometric.ts @@ -0,0 +1,46 @@ +import { check, PERMISSIONS, request, RESULTS } from 'react-native-permissions'; +import { Alert, Linking, Platform } from 'react-native'; +import { AutoLockState } from 'utils/autoLock'; +import i18n from 'utils/i18n/i18n'; + +export const requestFaceIDPermission = async (onPressCancel?: () => void) => { + if (Platform.OS === 'android') { + return; + } + try { + AutoLockState.isPreventAutoLock = true; + const result = await check(PERMISSIONS.IOS.FACE_ID); + AutoLockState.isPreventAutoLock = false; + + switch (result) { + case RESULTS.UNAVAILABLE: + // Images: This feature is not available (on this device / in this context) + break; + case RESULTS.DENIED: + request(PERMISSIONS.IOS.FACE_ID).then(() => onPressCancel && onPressCancel()); + // Images: The permission has not been requested / is denied but requestable + break; + case RESULTS.GRANTED: + // Images: The permission is granted + return result; + case RESULTS.BLOCKED: + Alert.alert(i18n.common.notify, i18n.common.noFaceIdPermission, [ + { + text: i18n.buttonTitles.cancel, + onPress: onPressCancel, + }, + { + text: i18n.common.goToSetting, + onPress: () => { + onPressCancel && onPressCancel(); + Linking.openSettings(); + }, + }, + ]); + return null; + } + } catch (e) { + console.log(e); + return null; + } +}; diff --git a/yarn.lock b/yarn.lock index 79b5cfd0b..eaa8fad4c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15110,10 +15110,10 @@ react-native-inappbrowser-reborn@^3.7.0: invariant "^2.2.4" opencollective-postinstall "^2.0.3" -react-native-keychain@^8.1.1: - version "8.1.1" - resolved "https://registry.yarnpkg.com/react-native-keychain/-/react-native-keychain-8.1.1.tgz#3bb5e37946b964a7bcf7df2fe470dd244e01a340" - integrity sha512-8fxgeHKwGcL657eAYpdBTkDIxNhbIHI+kyyO0Yac2dgVAN184JoIwQcW2z6snahwDaCObQOu0biLFHnsH+4KSg== +react-native-keychain@^8.1.2: + version "8.1.2" + resolved "https://registry.yarnpkg.com/react-native-keychain/-/react-native-keychain-8.1.2.tgz#34291ae472878e5124d081211af5ede7d810e64f" + integrity sha512-bhHEui+yMp3Us41NMoRGtnWEJiBE0g8tw5VFpq4mpmXAx6XJYahuM6K3WN5CsUeUl83hYysSL9oFZNKSTPSvYw== react-native-linear-gradient@^2.6.2: version "2.8.0" @@ -15239,6 +15239,11 @@ react-native-screens@^3.19.0: react-freeze "^1.0.0" warn-once "^0.1.0" +react-native-sensitive-info@^6.0.0-alpha.9: + version "6.0.0-alpha.9" + resolved "https://registry.yarnpkg.com/react-native-sensitive-info/-/react-native-sensitive-info-6.0.0-alpha.9.tgz#b488fab715d36efdf80b377a21545cfe888ab8d2" + integrity sha512-W2R1gUHgBAmy+wVl0BvA7s01SqqTitnG4Sdj2zmck0OS5a54kq+pTnXRqDljMa2NQs4Mue2IHyvd+Tf0X1ZL6Q== + react-native-size-matters@^0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/react-native-size-matters/-/react-native-size-matters-0.3.1.tgz#24d0cfc335a2c730f6d58bd7b43ea5a41be4b49f" @@ -15294,11 +15299,6 @@ react-native-toast-notifications@^3.3.1: resolved "https://registry.yarnpkg.com/react-native-toast-notifications/-/react-native-toast-notifications-3.3.1.tgz#c3d6f3b63a4df81c2912560d27878ea056672981" integrity sha512-yc1Q2nOdIYvAf0GAIlmg8q42hiwpEHnLxkxJ6P+tN6jpcKZ1qzMXlgnmNdyF9cm9VOyHQexEP8952IKNAv1Olw== -react-native-touch-id@^4.4.1: - version "4.4.1" - resolved "https://registry.yarnpkg.com/react-native-touch-id/-/react-native-touch-id-4.4.1.tgz#8b1bb2d04c30bac36bb9696d2d723e719c4a8b08" - integrity sha512-1jTl8fC+0fxvqegy/XXTyo6vMvPhjzkoDdaqoYZx0OH8AT250NuXnNPyKktvigIcys3+2acciqOeaCall7lrvg== - react-native-vector-icons@^9.2.0: version "9.2.0" resolved "https://registry.yarnpkg.com/react-native-vector-icons/-/react-native-vector-icons-9.2.0.tgz#3c0c82e95defd274d56363cbe8fead8d53167ebd"