Skip to content

Commit

Permalink
[issue-444, issue-430] Save Master Password in Keychain & Update Logi…
Browse files Browse the repository at this point in the history
…n screen
  • Loading branch information
nguyenduythuc authored and dominhquang committed Jun 29, 2023
1 parent a83e4d7 commit e0520f6
Show file tree
Hide file tree
Showing 16 changed files with 404 additions and 98 deletions.
31 changes: 16 additions & 15 deletions src/AppNew.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import useAppLock from 'hooks/useAppLock';
import useCryptoReady from 'hooks/init/useCryptoReady';
import useSetupI18n from 'hooks/init/useSetupI18n';
import SplashScreen from 'react-native-splash-screen';
import { LockScreen } from 'screens/LockScreen';
import Login from 'screens/MasterPassword/Login';
import { LoadingScreen } from 'screens/LoadingScreen';
import { ColorMap } from 'styles/color';
import { AutoLockState } from 'utils/autoLock';
Expand Down Expand Up @@ -46,22 +46,23 @@ const layerScreenStyle: StyleProp<any> = {
};

AutoLockState.isPreventAutoLock = false;
const autoLockParams: { pinCodeEnabled: boolean; faceIdEnabled: boolean; autoLockTime?: number; lock: () => void } = {
pinCodeEnabled: false,
faceIdEnabled: false,
autoLockTime: undefined,
lock: () => {},
};
const autoLockParams: { hasMasterPassword: boolean; faceIdEnabled: boolean; autoLockTime?: number; lock: () => void } =
{
hasMasterPassword: false,
faceIdEnabled: false,
autoLockTime: undefined,
lock: () => {},
};
let timeout: NodeJS.Timeout | undefined;
let lockWhenActive = false;
AppState.addEventListener('change', (state: string) => {
const { pinCodeEnabled, faceIdEnabled, autoLockTime, lock } = autoLockParams;
const { faceIdEnabled, autoLockTime, lock } = autoLockParams;

if (state === 'background') {
keyringLock().catch((e: Error) => console.log(e));
}

if (!pinCodeEnabled || autoLockTime === undefined) {
if (autoLockTime === undefined) {
return;
}

Expand Down Expand Up @@ -96,7 +97,7 @@ export const AppNew = () => {
const theme = isDarkMode ? THEME_PRESET.dark : THEME_PRESET.light;
StatusBar.setBarStyle(isDarkMode ? 'light-content' : 'dark-content');

const { pinCodeEnabled, faceIdEnabled, autoLockTime } = useSelector((state: RootState) => state.mobileSettings);
const { faceIdEnabled, autoLockTime } = useSelector((state: RootState) => state.mobileSettings);
const { hasMasterPassword } = useSelector((state: RootState) => state.accountState);
const { buildNumber } = useSelector((state: RootState) => state.appVersion);
const { isLocked, lock } = useAppLock();
Expand All @@ -108,18 +109,18 @@ export const AppNew = () => {

// Enable lock screen on the start app
useEffect(() => {
if (!firstTimeCheckPincode && pinCodeEnabled) {
if (!firstTimeCheckPincode) {
lock();
}
firstTimeCheckPincode = true;
}, [lock, pinCodeEnabled]);
}, [lock]);

useEffect(() => {
autoLockParams.lock = lock;
autoLockParams.autoLockTime = autoLockTime;
autoLockParams.pinCodeEnabled = pinCodeEnabled;
autoLockParams.hasMasterPassword = hasMasterPassword;
autoLockParams.faceIdEnabled = faceIdEnabled;
}, [autoLockTime, faceIdEnabled, lock, pinCodeEnabled]);
}, [autoLockTime, faceIdEnabled, lock, hasMasterPassword]);

const isRequiredStoresReady = true;

Expand Down Expand Up @@ -177,7 +178,7 @@ export const AppNew = () => {
)}
{isLocked && (
<View style={layerScreenStyle}>
<LockScreen />
<Login />
</View>
)}
</View>
Expand Down
2 changes: 2 additions & 0 deletions src/assets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ 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'));

export const SVGImages = {
Logo,
LogoGradient,
SubwalletStyled,
CheckBoxIcon: CheckBoxIcon,
CheckBoxFilledIcon: CheckBoxFilledIcon,
NftIcon: NftIcon,
Expand Down
3 changes: 3 additions & 0 deletions src/assets/subwallet-styled.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
113 changes: 113 additions & 0 deletions src/components/common/Field/Password/InlinePassword.tsx
Original file line number Diff line number Diff line change
@@ -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<TextInput>) => {
const {
defaultValue,
onChangeText,
onEndEditing,
onBlur,
errorMessages,
isBusy,
autoFocus,
onSubmitField,
showEyeButton = true,
placeholder,
containerStyle,
disabled,
} = passwordFieldProps;
const [isShowPassword, setShowPassword] = useState<boolean>(false);
const [isFocus, setFocus] = useState<boolean>(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 (
<>
<View style={[styles.inputContainer, containerStyle, disabled && DisabledStyle]}>
<Input
ref={ref}
leftPart={<Icon phosphorIcon={Key} weight={'regular'} size={'sm'} iconColor={theme['gray-5']} />}
leftPartStyle={styles.leftInputStyle}
inputStyle={styles.textInput}
rightPart={
showEyeButton &&
(isShowPassword ? (
<Button
disabled={isBusy}
onPress={() => setShowPassword(false)}
size={'xs'}
type={'ghost'}
icon={<Icon phosphorIcon={Eye} weight={'regular'} size={'sm'} iconColor={theme['gray-5']} />}
/>
) : (
<Button
disabled={isBusy}
onPress={() => setShowPassword(true)}
size={'xs'}
type={'ghost'}
icon={<Icon phosphorIcon={EyeSlash} weight={'regular'} size={'sm'} iconColor={theme['gray-5']} />}
/>
))
}
rightPartStyle={styles.rightInputStyle}
isError={!!(errorMessages && errorMessages.length)}
placeholder={placeholder}
autoFocus={autoFocus}
autoCorrect={false}
placeholderTextColor={theme.colorTextTertiary}
selectionColor={theme.colorTextDisabled}
secureTextEntry={!isShowPassword}
blurOnSubmit={false}
textContentType="oneTimeCode"
selectTextOnFocus={!isBusy}
onSubmitEditing={onSubmitField}
onChangeText={onChangeText}
onEndEditing={onEndEditing}
onBlur={onInputBlur}
onFocus={onInputFocus}
defaultValue={defaultValue || ''}
/>
</View>

{!!(errorMessages && errorMessages.length) &&
errorMessages.map((message, index) => (
<Warning key={index} isDanger message={message} style={styles.warningStyle} />
))}
</>
);
});

export default InlinePassword;
1 change: 1 addition & 0 deletions src/components/common/Field/Password/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as InlinePassword } from './InlinePassword';
42 changes: 42 additions & 0 deletions src/components/common/Field/Password/styles/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Platform, StyleSheet, TextStyle, ViewStyle } from 'react-native';
import { ThemeTypes } from 'styles/themes';
import { FontMedium } from 'styles/sharedStyles';

export interface ComponentStyle {
inputContainer: ViewStyle;
textInput: TextStyle;
leftInputStyle: ViewStyle;
rightInputStyle: ViewStyle;
warningStyle: ViewStyle;
}

function createStyles(theme: ThemeTypes, isValid: boolean, readonly?: boolean, isFocus?: boolean) {
return StyleSheet.create<ComponentStyle>({
inputContainer: {
width: '100%',
position: 'relative',
marginBottom: 8,
height: 48,
},
textInput: {
...FontMedium,
paddingLeft: 44,
paddingRight: 52,
paddingTop: 13,
paddingBottom: 13,
borderColor: isFocus ? theme.colorPrimary : theme.colorBgSecondary,
borderWidth: 2,
borderRadius: theme.borderRadiusLG,
backgroundColor: theme.colorBgSecondary,
color: isValid ? (readonly ? theme.colorTextLight5 : theme.colorTextLight1) : theme.colorError,
lineHeight: Platform.OS === 'ios' ? 17 : theme.lineHeight * theme.fontSize,
fontSize: theme.fontSize,
zIndex: 1,
},
leftInputStyle: { left: 15, zIndex: 2 },
rightInputStyle: { right: 3, zIndex: 2 },
warningStyle: { marginBottom: 8 },
});
}

export default createStyles;
4 changes: 2 additions & 2 deletions src/components/common/Modal/UnlockModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { useSubWalletTheme } from 'hooks/useSubWalletTheme';
import createStyle from './style';

interface Props {
onPasswordComplete: VoidFunction;
onPasswordComplete: (password: string) => void;
visible: boolean;
onHideModal: VoidFunction;
}
Expand Down Expand Up @@ -44,7 +44,7 @@ export const UnlockModal: React.FC<Props> = (props: Props) => {
if (!data.status) {
onUpdateErrors('password')([i18n.errorMessage.invalidMasterPassword]);
} else {
onPasswordComplete();
onPasswordComplete(password);
}
})
.catch((e: Error) => {
Expand Down
40 changes: 30 additions & 10 deletions src/hooks/modal/useUnlockModal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { useSelector } from 'react-redux';
import { RootState } from 'stores/index';
import { VoidFunction } from 'types/index';
import { noop } from 'utils/function';
import { keyringUnlock } from 'messaging/index';
import * as Keychain from 'react-native-keychain';

interface Result {
visible: boolean;
Expand All @@ -12,14 +14,38 @@ interface Result {
}

const useUnlockModal = (): Result => {
const { isLocked, hasMasterPassword } = useSelector((state: RootState) => state.accountState);
const { hasMasterPassword } = useSelector((state: RootState) => state.accountState);

const [visible, setVisible] = useState(false);
const onCompleteRef = useRef<VoidFunction>(noop);
const promiseRef = useRef<Promise<boolean> | undefined>();
const resolveRef = useRef<(value: boolean | PromiseLike<boolean>) => void>();
const rejectRef = useRef<(reason?: any) => void>();

async function handleUnlockPassword() {
try {
const credential = await Keychain.getGenericPassword({
accessControl: Keychain.ACCESS_CONTROL.BIOMETRY_ANY,
});
if (!credential) {
setVisible(true);
promiseRef.current = new Promise<boolean>((resolve, reject) => {
resolveRef.current = resolve;
rejectRef.current = reject;
});

return;
}

const unlockData = await keyringUnlock({ password: credential.password });
if (unlockData.status) {
onCompleteRef.current();
}
} catch (e) {
console.warn('unlock failed:', e);
}
}

const onPress = useCallback(
(onComplete: VoidFunction): (() => Promise<boolean> | undefined) => {
return () => {
Expand All @@ -28,22 +54,16 @@ const useUnlockModal = (): Result => {
} else {
onCompleteRef.current = onComplete;

if (hasMasterPassword && isLocked) {
setVisible(true);
promiseRef.current = new Promise<boolean>((resolve, reject) => {
resolveRef.current = resolve;
rejectRef.current = reject;
});

return promiseRef.current;
if (hasMasterPassword) {
handleUnlockPassword();
} else {
onCompleteRef.current();
return Promise.resolve(true);
}
}
};
},
[hasMasterPassword, isLocked],
[hasMasterPassword],
);

const onPasswordComplete = useCallback(() => {
Expand Down
6 changes: 3 additions & 3 deletions src/hooks/useAppLock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { updateLockState } from 'stores/AppState';
export interface UseAppLockOptions {
isLocked: boolean;
unlock: (code: string) => boolean;
unlockWithBiometric: () => void;
unlockApp: () => void;
lock: () => void;
}

Expand All @@ -25,13 +25,13 @@ export default function useAppLock(): UseAppLockOptions {
[dispatch, pinCode],
);

const unlockWithBiometric = useCallback(() => {
const unlockApp = useCallback(() => {
dispatch(updateLockState(false));
}, [dispatch]);

const lock = useCallback(() => {
dispatch(updateLockState(true));
}, [dispatch]);

return { isLocked, unlock, lock, unlockWithBiometric };
return { isLocked, unlock, lock, unlockApp };
}
Loading

0 comments on commit e0520f6

Please sign in to comment.