Unverified Commit 67fd659d authored by Hanwen Cheng's avatar Hanwen Cheng Committed by GitHub
Browse files

feat: add passworded identity (#565)

* fix: upgrade react navigation v5

* update project settings in ios

* improve navigation by upgrading to react navigation v5

* make compiler happy

* unlink react-native-secure-storage

* make detox and react-navigation happy again

* make e2e test happy

* use safe area context app wide

* delete stray comment

* fix screen heading styles

* fix pin backup navigation

* revert change to rust target

* fix ui overlap on android

* remove bounce in ios scroll view

* lint fix

* feat: enable passworded identity

* use keyboard scroll view

* complete password generation

* update react-native-camera and related polkadot api packages

* add registry store and use type override

* reduce android bundle size

* update yarn.lock

* update metadata

* prettier happy

* update polkadot api

* add password in pinInputt

* remove password from identity

* add password in path derivation

* remove log

* complete password support

* make compiler happy

* refactor account store error handling

* remove password check when signing

* add lock icon for passworded account

* add hint also on path

* add extra hint text

* fix autofocus and remove useRef

* add e2e test suit for passworded account

* make lint happy

* add path error indicator
parent 6b050c2f
Pipeline #88873 failed with stages
in 3 minutes and 39 seconds
......@@ -22,6 +22,7 @@ module.exports = {
components: './src/components',
constants: './src/constants',
e2e: './test/e2e',
modules: './src/modules',
res: './res',
screens: './src/screens',
stores: './src/stores',
......
......@@ -44,6 +44,7 @@ export default function App(props: AppProps): React.ReactElement {
'Warning: componentWillMount',
'Warning: componentWillUpdate',
'Sending `onAnimatedValueUpdate`',
'MenuProviders',
'Non-serializable values were found in the navigation state' // https://reactnavigation.org/docs/troubleshooting/#i-get-the-warning-non-serializable-values-were-found-in-the-navigation-state
]);
}
......
// Copyright 2015-2019 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { useState } from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import Icon from 'react-native-vector-icons/AntDesign';
import testIDs from 'e2e/testIDs';
import TextInput from 'components/TextInput';
import fontStyles from 'styles/fontStyles';
import { passwordRegex } from 'utils/regex';
export default function PasswordInput({
password,
setPassword,
onSubmitEditing
}: {
password: string;
setPassword: (newPassword: string) => void;
onSubmitEditing: () => void;
}): React.ReactElement {
const onPasswordChange = (newPassword: string): void => {
if (passwordRegex.test(newPassword)) setPassword(newPassword);
};
const [isShow, setShow] = useState<boolean>(false);
const togglePasswordInput = (): void => setShow(!isShow);
return (
<View style={styles.container}>
<TouchableOpacity
onPress={togglePasswordInput}
style={styles.label}
testID={testIDs.PathDerivation.togglePasswordButton}
>
<Text style={fontStyles.t_regular}>Add Optional Password</Text>
<Icon
name={isShow ? 'caretup' : 'caretdown'}
style={styles.labelIcon}
/>
</TouchableOpacity>
{isShow && (
<>
<TextInput
onChangeText={onPasswordChange}
testID={testIDs.PathDerivation.passwordInput}
returnKeyType="done"
onSubmitEditing={onSubmitEditing}
placeholder="Optional password"
value={password}
/>
<Text style={styles.hintText}>
Password will be always needed when signing with this account.
</Text>
</>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
marginBottom: 16
},
hintText: {
...fontStyles.t_regular,
paddingHorizontal: 16
},
label: {
alignItems: 'center',
flexDirection: 'row',
marginBottom: 3,
paddingHorizontal: 16
},
labelIcon: {
paddingLeft: 8,
...fontStyles.t_regular
}
});
......@@ -65,6 +65,7 @@ export default function PathCard({
const address = getAddressWithPath(path, identity);
const isUnknownAddress = address === '';
const hasPassword = identity.meta.get(path)?.hasPassword ?? false;
const computedNetworkKey =
networkKey || getNetworkKeyByPath(path, identity.meta.get(path)!);
const networkParams =
......@@ -86,7 +87,7 @@ export default function PathCard({
<AccountIcon
address={address}
network={networkParams}
style={styles.icon}
style={styles.iconUser}
/>
<View style={styles.desc}>
<View>
......@@ -114,19 +115,26 @@ export default function PathCard({
accessibilityComponentType="button"
disabled={false}
onPress={onPress}
testID={testID}
>
<View style={[styles.content, styles.contentDer]}>
<View style={[styles.content, styles.contentSubstrate]} testID={testID}>
<AccountIcon
address={address}
network={networkParams}
style={styles.icon}
style={styles.iconUser}
/>
<View style={styles.desc}>
<AccountPrefixedTitle title={pathName!} titlePrefix={titlePrefix} />
<View style={styles.titleContainer}>
<AccountPrefixedTitle
title={pathName!}
titlePrefix={titlePrefix}
/>
{hasPassword && <AntIcon name="lock" style={styles.iconLock} />}
</View>
<View style={{ alignItems: 'center', flexDirection: 'row' }}>
<AntIcon name="user" size={10} color={colors.bg_text_sec} />
<Text style={fontStyles.t_codeS}>{path}</Text>
<Text style={fontStyles.t_codeS}>
{hasPassword ? `${path}///***` : path}
</Text>
</View>
{address !== '' && (
<Text
......@@ -159,7 +167,7 @@ const styles = StyleSheet.create({
flexDirection: 'row',
paddingLeft: 16
},
contentDer: {
contentSubstrate: {
backgroundColor: colors.card_bg,
paddingVertical: 8
},
......@@ -176,8 +184,16 @@ const styles = StyleSheet.create({
marginLeft: 8,
width: 8
},
icon: {
iconLock: {
marginLeft: 4,
...fontStyles.h2
},
iconUser: {
height: 40,
width: 40
},
titleContainer: {
alignItems: 'center',
flexDirection: 'row'
}
});
......@@ -74,6 +74,7 @@ export default class TextInput extends React.PureComponent<Props, {}> {
)}
<TextInputOrigin
ref={(input: TextInputOrigin): any => (this.input = input)}
autoCapitalize="none"
keyboardAppearance="dark"
underlineColorAndroid="transparent"
{...this.props}
......
// Copyright 2015-2020 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React from 'react';
import styles from '../styles';
import KeyboardScrollView from 'components/KeyboardScrollView';
import testIDs from 'e2e/testIDs';
export default function Container(props: any): React.ReactElement {
return (
<KeyboardScrollView
{...props}
style={styles.body}
extraHeight={200}
testID={testIDs.IdentityPin.scrollScreen}
/>
);
}
// Copyright 2015-2019 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { MutableRefObject } from 'react';
import { StyleSheet, TextInputProps } from 'react-native';
import styles from '../styles';
import TextInput from 'components/TextInput';
import fontStyles from 'styles/fontStyles';
interface PinInputProps extends TextInputProps {
label: string;
focus?: boolean;
ref?: MutableRefObject<TextInput | null>;
}
export default function PinInput(props: PinInputProps): React.ReactElement {
return (
<TextInput
keyboardAppearance="dark"
editable
keyboardType="numeric"
multiline={false}
autoCorrect={false}
numberOfLines={1}
returnKeyType="next"
secureTextEntry
{...props}
style={StyleSheet.flatten([
fontStyles.t_seed,
styles.pinInput,
{ fontSize: 22 },
props.style
])}
/>
);
}
// Copyright 2015-2020 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { useState } from 'react';
import { State, StateReducer, UpdateStateFunc } from './types';
const initialState: State = {
confirmation: '',
focusConfirmation: false,
password: '',
pin: '',
pinMismatch: false,
pinTooShort: false
};
export function usePinState(): StateReducer {
const [state, setState] = useState<State>(initialState);
const updateState: UpdateStateFunc = delta =>
setState({ ...state, ...delta });
const resetState = (): void => setState(initialState);
return [state, updateState, resetState];
}
// Copyright 2015-2020 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React from 'react';
import ButtonMainAction from 'components/ButtonMainAction';
import ScreenHeading from 'components/ScreenHeading';
import testIDs from 'e2e/testIDs';
import Container from 'modules/unlock/components/Container';
import PinInput from 'modules/unlock/components/PinInput';
import { usePinState } from 'modules/unlock/hooks';
import t from 'modules/unlock/strings';
import { getSubtitle, onPinInputChange } from 'modules/unlock/utils';
import { NavigationProps } from 'types/props';
export default function PinNew({
route
}: NavigationProps<'PinNew'>): React.ReactElement {
const [state, updateState, resetState] = usePinState();
function submit(): void {
const { pin, confirmation } = state;
if (pin.length >= 6 && pin === confirmation) {
const resolve = route.params.resolve;
resetState();
resolve(pin);
} else {
if (pin.length < 6) {
updateState({ pinTooShort: true });
} else if (pin !== confirmation) updateState({ pinMismatch: true });
}
}
return (
<Container>
<ScreenHeading
title={t.title.pinCreation}
subtitle={getSubtitle(state, false)}
error={state.pinMismatch || state.pinTooShort}
/>
<PinInput
label={t.pinLabel}
autoFocus
testID={testIDs.IdentityPin.setPin}
returnKeyType="next"
onFocus={(): void => updateState({ focusConfirmation: false })}
onSubmitEditing={(): void => {
updateState({ focusConfirmation: true });
}}
onChangeText={onPinInputChange('pin', updateState)}
value={state.pin}
/>
<PinInput
label={t.pinConfirmLabel}
returnKeyType="done"
testID={testIDs.IdentityPin.confirmPin}
focus={state.focusConfirmation}
onChangeText={onPinInputChange('confirmation', updateState)}
value={state.confirmation}
onSubmitEditing={submit}
/>
<ButtonMainAction
title={t.doneButton.pinCreation}
bottom={false}
onPress={submit}
testID={testIDs.IdentityPin.submitButton}
/>
</Container>
);
}
// Copyright 2015-2020 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React from 'react';
import Container from 'modules/unlock/components/Container';
import PinInput from 'modules/unlock/components/PinInput';
import { usePinState } from 'modules/unlock/hooks';
import t from 'modules/unlock/strings';
import { getSubtitle, onPinInputChange } from 'modules/unlock/utils';
import testIDs from 'e2e/testIDs';
import ScreenHeading from 'components/ScreenHeading';
import ButtonMainAction from 'components/ButtonMainAction';
import { NavigationAccountProps } from 'types/props';
import { withAccountStore } from 'utils/HOC';
import { unlockIdentitySeed } from 'utils/identitiesUtils';
function PinUnlock({
accounts,
route
}: NavigationAccountProps<'PinUnlock'>): React.ReactElement {
const [state, updateState, resetState] = usePinState();
const targetIdentity =
route.params.identity ?? accounts.state.currentIdentity;
async function submit(): Promise<void> {
const { pin } = state;
if (pin.length >= 6 && targetIdentity) {
try {
const resolve = route.params.resolve;
const seedPhrase = await unlockIdentitySeed(pin, targetIdentity);
resetState();
resolve(seedPhrase);
} catch (e) {
updateState({ pin: '', pinMismatch: true });
//TODO record error times;
}
} else {
updateState({ pinTooShort: true });
}
}
return (
<Container>
<ScreenHeading
title={t.title.pinUnlock}
error={state.pinMismatch || state.pinTooShort}
subtitle={getSubtitle(state, true)}
/>
<PinInput
label={t.pinLabel}
autoFocus
testID={testIDs.IdentityPin.unlockPinInput}
returnKeyType="done"
onChangeText={onPinInputChange('pin', updateState)}
onSubmitEditing={submit}
value={state.pin}
/>
<ButtonMainAction
title={t.doneButton.pinUnlock}
bottom={false}
onPress={submit}
testID={testIDs.IdentityPin.unlockPinButton}
/>
</Container>
);
}
export default withAccountStore(PinUnlock);
// Copyright 2015-2020 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { useState } from 'react';
import Container from 'modules/unlock/components/Container';
import PinInput from 'modules/unlock/components/PinInput';
import { usePinState } from 'modules/unlock/hooks';
import t from 'modules/unlock/strings';
import { getSubtitle, onPinInputChange } from 'modules/unlock/utils';
import testIDs from 'e2e/testIDs';
import ScreenHeading from 'components/ScreenHeading';
import ButtonMainAction from 'components/ButtonMainAction';
import { NavigationAccountProps } from 'types/props';
import { withAccountStore } from 'utils/HOC';
import { unlockIdentitySeed } from 'utils/identitiesUtils';
import { constructSURI } from 'utils/suri';
function PinUnlockWithPassword({
accounts,
route
}: NavigationAccountProps<'PinUnlockWithPassword'>): React.ReactElement {
const [state, updateState, resetState] = usePinState();
const [focusPassword, setFocusPassword] = useState<boolean>(false);
const targetIdentity =
route.params.identity ?? accounts.state.currentIdentity;
async function submit(): Promise<void> {
const { pin, password } = state;
const derivePath = route.params.path;
if (pin.length >= 6 && targetIdentity) {
try {
const resolve = route.params.resolve;
const seedPhrase = await unlockIdentitySeed(pin, targetIdentity);
const suri = constructSURI({
derivePath,
password,
phrase: seedPhrase
});
resetState();
resolve(suri);
} catch (e) {
updateState({ password: '', pin: '', pinMismatch: true });
//TODO record error times;
}
} else {
updateState({ pinTooShort: true });
}
}
function onPasswordInputChange(password: string): void {
updateState({
password,
pinMismatch: false
});
}
return (
<Container>
<ScreenHeading
title={t.title.pinUnlock}
error={state.pinMismatch || state.pinTooShort}
subtitle={getSubtitle(state, true)}
/>
<PinInput
label={t.pinLabel}
autoFocus
testID={testIDs.IdentityPin.unlockPinInput}
returnKeyType="done"