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

feat: change identicon when derivate path (#602)

* 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

* destroy reference when app go into background

* fix lint

* add seed ref functions

* enable pin los address creation

* signing with seed reference

* fix bridge in ios

* use lists for data pointers for each identity

* createSeedRefWithNewSeed function

* fix logic error

* more fix and complete e2e test

* remove console log

* add copyright

* fix lint errors

* rebase commit on master
parent 78a1b5d6
Pipeline #90814 failed with stages
in 3 minutes and 40 seconds
......@@ -14,19 +14,16 @@
// 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 React, { useEffect, useState } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import AntIcon from 'react-native-vector-icons/AntDesign';
import AccountIcon from './AccountIcon';
import AccountPrefixedTitle from './AccountPrefixedTitle';
import Address from './Address';
import TouchableItem from './TouchableItem';
import AccountPrefixedTitle from './AccountPrefixedTitle';
import {
isSubstrateNetworkParams,
isUnknownNetworkParams
} from 'types/networkSpecsTypes';
import Separator from 'components/Separator';
import {
defaultNetworkKey,
NETWORK_LIST,
......@@ -34,18 +31,24 @@ import {
} from 'constants/networkSpecs';
import colors from 'styles/colors';
import fontStyles from 'styles/fontStyles';
import Separator from 'components/Separator';
import { Identity } from 'types/identityTypes';
import {
isSubstrateNetworkParams,
isUnknownNetworkParams,
SubstrateNetworkParams
} from 'types/networkSpecsTypes';
import { ButtonListener } from 'types/props';
import {
getAddressWithPath,
getNetworkKeyByPath,
getPathName
} from 'utils/identitiesUtils';
import { ButtonListener } from 'types/props';
import { Identity } from 'types/identityTypes';
import { useSeedRef } from 'utils/seedRefHooks';
export default function PathCard({
onPress,
identity,
isPathValid = true,
path,
name,
networkKey,
......@@ -54,6 +57,7 @@ export default function PathCard({
}: {
onPress?: ButtonListener;
identity: Identity;
isPathValid?: boolean;
path: string;
name?: string;
networkKey?: string;
......@@ -62,12 +66,38 @@ export default function PathCard({
}): React.ReactElement {
const isNotEmptyName = name && name !== '';
const pathName = isNotEmptyName ? name : getPathName(path, identity);
const address = getAddressWithPath(path, identity);
const isUnknownAddress = address === '';
const hasPassword = identity.meta.get(path)?.hasPassword ?? false;
const { isSeedRefValid, substrateAddress } = useSeedRef(
identity.encryptedSeed
);
const [address, setAddress] = useState('');
const computedNetworkKey =
networkKey || getNetworkKeyByPath(path, identity.meta.get(path)!);
useEffect(() => {
const getAddress = async (): Promise<void> => {
const existedAddress = getAddressWithPath(path, identity);
if (existedAddress !== '') return setAddress(existedAddress);
if (isSeedRefValid && isPathValid) {
const prefix = (NETWORK_LIST[
computedNetworkKey
] as SubstrateNetworkParams).prefix;
const generatedAddress = await substrateAddress(path, prefix);
return setAddress(generatedAddress);
}
setAddress('');
};
getAddress();
}, [
path,
identity,
isPathValid,
networkKey,
computedNetworkKey,
isSeedRefValid,
substrateAddress
]);
const isUnknownAddress = address === '';
const hasPassword = identity.meta.get(path)?.hasPassword ?? false;
const networkParams =
computedNetworkKey === UnknownNetworkKeys.UNKNOWN && !isUnknownAddress
? NETWORK_LIST[defaultNetworkKey]
......
......@@ -36,7 +36,7 @@ import {
NetworkParams,
SubstrateNetworkParams
} from 'types/networkSpecsTypes';
import { NavigationAccountProps } from 'types/props';
import { NavigationAccountIdentityProps } from 'types/props';
import { alertPathDerivationError } from 'utils/alertUtils';
import {
getExistedNetworkKeys,
......@@ -44,6 +44,7 @@ import {
getPathsWithSubstrateNetworkKey
} from 'utils/identitiesUtils';
import {
navigateToPathDerivation,
navigateToPathDetails,
navigateToPathsList,
unlockSeedPhrase
......@@ -63,11 +64,11 @@ export default function NetworkSelector({
accounts,
navigation,
route
}: NavigationAccountProps<'Main'>): React.ReactElement {
}: NavigationAccountIdentityProps<'Main'>): React.ReactElement {
const isNew = route.params?.isNew ?? false;
const [shouldShowMoreNetworks, setShouldShowMoreNetworks] = useState(false);
const { identities, currentIdentity } = accounts.state;
const seedRefHooks = useSeedRef(currentIdentity!.encryptedSeed);
const seedRefHooks = useSeedRef(currentIdentity.encryptedSeed);
// catch android back button and prevent exiting the app
useFocusEffect(
......@@ -149,8 +150,8 @@ export default function NetworkSelector({
const renderCustomPathCard = (): React.ReactElement => (
<NetworkCard
isAdd={true}
onPress={(): void =>
navigation.navigate('PathDerivation', { parentPath: '' })
onPress={(): Promise<void> =>
navigateToPathDerivation(navigation, '', seedRefHooks.isSeedRefValid)
}
testID={testIDs.Main.addCustomNetworkButton}
title="Create Custom Path"
......@@ -186,7 +187,7 @@ export default function NetworkSelector({
/>
);
} else {
const identityName = getIdentityName(currentIdentity!, identities);
const identityName = getIdentityName(currentIdentity, identities);
return <IdentityHeading title={identityName} />;
}
};
......@@ -202,13 +203,13 @@ export default function NetworkSelector({
await deriveEthereumAccount(networkKey);
}
} else {
const paths = Array.from(currentIdentity!.meta.keys());
const paths = Array.from(currentIdentity.meta.keys());
if (
isSubstrateNetworkParams(networkParams) ||
isUnknownNetworkParams(networkParams)
) {
const listedPaths = getPathsWithSubstrateNetworkKey(
currentIdentity!,
currentIdentity,
networkKey
);
if (listedPaths.length === 0 && isSubstrateNetworkParams(networkParams))
......@@ -226,7 +227,7 @@ export default function NetworkSelector({
}
};
const availableNetworks = getExistedNetworkKeys(currentIdentity!);
const availableNetworks = getExistedNetworkKeys(currentIdentity);
const networkList = Object.entries(NETWORK_LIST).filter(filterNetworkKeys);
networkList.sort(sortNetworkKeys);
......
......@@ -20,7 +20,10 @@ import NoCurrentIdentity from 'modules/main/components/NoCurrentIdentity';
import { SafeAreaViewContainer } from 'components/SafeAreaContainer';
import OnBoardingView from 'modules/main/components/OnBoading';
import NetworkSelector from 'modules/main/components/NetworkSelector';
import { NavigationAccountProps } from 'types/props';
import {
NavigationAccountIdentityProps,
NavigationAccountProps
} from 'types/props';
import { withAccountStore } from 'utils/HOC';
function Main(props: NavigationAccountProps<'Main'>): React.ReactElement {
......@@ -30,8 +33,10 @@ function Main(props: NavigationAccountProps<'Main'>): React.ReactElement {
if (!loaded) return <SafeAreaViewContainer />;
if (identities.length === 0)
return <OnBoardingView hasLegacyAccount={hasLegacyAccount} />;
if (!currentIdentity) return <NoCurrentIdentity />;
return <NetworkSelector {...props} />;
if (currentIdentity === null) return <NoCurrentIdentity />;
return (
<NetworkSelector {...(props as NavigationAccountIdentityProps<'Main'>)} />
);
}
export default withAccountStore(Main);
......@@ -23,18 +23,16 @@ 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 { NavigationTargetIdentityProps } from 'types/props';
import { withAccountStore, withTargetIdentity } from 'utils/HOC';
import { unlockIdentitySeedWithReturn } from 'utils/identitiesUtils';
import { useSeedRef } from 'utils/seedRefHooks';
function PinUnlock({
accounts,
targetIdentity,
route
}: NavigationAccountProps<'PinUnlock'>): React.ReactElement {
}: NavigationTargetIdentityProps<'PinUnlock'>): React.ReactElement {
const [state, updateState, resetState] = usePinState();
const targetIdentity =
route.params.identity ?? accounts.state.currentIdentity!;
const { createSeedRef } = useSeedRef(targetIdentity.encryptedSeed);
async function submit(): Promise<void> {
......
......@@ -23,18 +23,16 @@ 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 { NavigationTargetIdentityProps } from 'types/props';
import { withAccountStore, withTargetIdentity } from 'utils/HOC';
import { useSeedRef } from 'utils/seedRefHooks';
function PinUnlockWithPassword({
accounts,
targetIdentity,
route
}: NavigationAccountProps<'PinUnlockWithPassword'>): React.ReactElement {
}: NavigationTargetIdentityProps<'PinUnlockWithPassword'>): React.ReactElement {
const [state, updateState, resetState] = usePinState();
const [focusPassword, setFocusPassword] = useState<boolean>(false);
const targetIdentity =
route.params.identity ?? accounts.state.currentIdentity!;
const { createSeedRef } = useSeedRef(targetIdentity.encryptedSeed);
async function submit(): Promise<void> {
......
......@@ -19,7 +19,7 @@ import { StyleSheet } from 'react-native';
import { SafeAreaViewContainer } from 'components/SafeAreaContainer';
import testIDs from 'e2e/testIDs';
import { NavigationAccountProps } from 'types/props';
import { NavigationAccountIdentityProps } from 'types/props';
import { withAccountStore, withCurrentIdentity } from 'utils/HOC';
import TextInput from 'components/TextInput';
import {
......@@ -37,14 +37,14 @@ import colors from 'styles/colors';
import PopupMenu from 'components/PopupMenu';
import { useSeedRef } from 'utils/seedRefHooks';
type Props = NavigationAccountProps<'IdentityManagement'>;
type Props = NavigationAccountIdentityProps<'IdentityManagement'>;
function IdentityManagement({
accounts,
navigation
}: Props): React.ReactElement {
const { currentIdentity } = accounts.state;
const { destroySeedRef } = useSeedRef(currentIdentity!.encryptedSeed);
const { destroySeedRef } = useSeedRef(currentIdentity.encryptedSeed);
const onRenameIdentity = async (name: string): Promise<void> => {
try {
......@@ -99,7 +99,7 @@ function IdentityManagement({
<TextInput
label="Display Name"
onChangeText={onRenameIdentity}
value={currentIdentity!.name}
value={currentIdentity.name}
placeholder="Enter a new identity name"
focus
/>
......
......@@ -20,7 +20,7 @@ import { Platform, StyleSheet, View } from 'react-native';
import PasswordInput from 'components/PasswordInput';
import testIDs from 'e2e/testIDs';
import { defaultNetworkKey, UnknownNetworkKeys } from 'constants/networkSpecs';
import { NavigationAccountProps } from 'types/props';
import { NavigationAccountIdentityProps } from 'types/props';
import { withAccountStore } from 'utils/HOC';
import TextInput from 'components/TextInput';
import ButtonMainAction from 'components/ButtonMainAction';
......@@ -39,13 +39,13 @@ function PathDerivation({
accounts,
navigation,
route
}: NavigationAccountProps<'PathDerivation'>): React.ReactElement {
}: NavigationAccountIdentityProps<'PathDerivation'>): React.ReactElement {
const [derivationPath, setDerivationPath] = useState<string>('');
const [keyPairsName, setKeyPairsName] = useState<string>('');
const [modalVisible, setModalVisible] = useState<boolean>(false);
const [password, setPassword] = useState<string>('');
const pathNameInput = useRef<TextInput>(null);
const currentIdentity = accounts.state.currentIdentity!;
const currentIdentity = accounts.state.currentIdentity;
const { isSeedRefValid, substrateAddress } = useSeedRef(
currentIdentity.encryptedSeed
);
......@@ -130,7 +130,8 @@ function PathDerivation({
onSubmitEditing={onPathDerivation}
/>
<PathCard
identity={accounts.state.currentIdentity!}
identity={accounts.state.currentIdentity}
isPathValid={isPathValid}
name={keyPairsName}
path={
password === '' ? completePath : `${completePath}///${password}`
......
......@@ -22,10 +22,10 @@ import { SafeAreaScrollViewContainer } from 'components/SafeAreaContainer';
import { defaultNetworkKey, UnknownNetworkKeys } from 'constants/networkSpecs';
import testIDs from 'e2e/testIDs';
// TODO use typescript 3.8's type import, Wait for prettier update.
import AccountsStore from 'stores/AccountsStore';
import { NavigationAccountProps } from 'types/props';
import { AccountsStoreStateWithIdentity } from 'types/identityTypes';
import { NavigationAccountIdentityProps } from 'types/props';
import { RootStackParamList } from 'types/routes';
import { withAccountStore } from 'utils/HOC';
import { withAccountStore, withCurrentIdentity } from 'utils/HOC';
import PathCard from 'components/PathCard';
import PopupMenu from 'components/PopupMenu';
import { LeftScreenHeading } from 'components/ScreenHeading';
......@@ -39,9 +39,13 @@ import {
isSubstratePath
} from 'utils/identitiesUtils';
import { alertDeleteAccount, alertPathDeletionError } from 'utils/alertUtils';
import { navigateToPathsList } from 'utils/navigationHelpers';
import {
navigateToPathDerivation,
navigateToPathsList
} from 'utils/navigationHelpers';
import { generateAccountId } from 'utils/account';
import UnknownAccountWarning from 'components/UnknownAccountWarning';
import { useSeedRef } from 'utils/seedRefHooks';
interface Props {
path: string;
......@@ -49,7 +53,7 @@ interface Props {
navigation:
| StackNavigationProp<RootStackParamList, 'PathDetails'>
| StackNavigationProp<RootStackParamList, 'PathsList'>;
accounts: AccountsStore;
accounts: AccountsStoreStateWithIdentity;
}
export function PathDetailsView({
......@@ -61,6 +65,7 @@ export function PathDetailsView({
const { currentIdentity } = accounts.state;
const address = getAddressWithPath(path, currentIdentity);
const accountName = getPathName(path, currentIdentity);
const { isSeedRefValid } = useSeedRef(currentIdentity.encryptedSeed);
if (!address) return <View />;
const isUnknownNetwork = networkKey === UnknownNetworkKeys.UNKNOWN;
const formattedNetworkKey = isUnknownNetwork ? defaultNetworkKey : networkKey;
......@@ -77,7 +82,7 @@ export function PathDetailsView({
await accounts.deletePath(path);
if (isSubstratePath(path)) {
const listedPaths = getPathsWithSubstrateNetworkKey(
accounts.state.currentIdentity!,
accounts.state.currentIdentity,
networkKey
);
const hasOtherPaths = listedPaths.length > 0;
......@@ -93,7 +98,7 @@ export function PathDetailsView({
});
break;
case 'PathDerivation':
navigation.navigate('PathDerivation', { parentPath: path });
navigateToPathDerivation(navigation, path, isSeedRefValid);
break;
case 'PathManagement':
navigation.navigate('PathManagement', { path });
......@@ -128,7 +133,7 @@ export function PathDetailsView({
/>
}
/>
<PathCard identity={currentIdentity!} path={path} />
<PathCard identity={currentIdentity} path={path} />
<QrView data={`${accountId}:${accountName}`} />
{isUnknownNetwork && <UnknownAccountWarning isPath />}
</SafeAreaScrollViewContainer>
......@@ -139,9 +144,9 @@ function PathDetails({
accounts,
navigation,
route
}: NavigationAccountProps<'PathDetails'>): React.ReactElement {
}: NavigationAccountIdentityProps<'PathDetails'>): React.ReactElement {
const path = route.params.path ?? '';
const networkKey = getNetworkKey(path, accounts.state.currentIdentity!);
const networkKey = getNetworkKey(path, accounts.state.currentIdentity);
return (
<PathDetailsView
accounts={accounts}
......@@ -158,4 +163,4 @@ const styles = StyleSheet.create({
}
});
export default withAccountStore(PathDetails);
export default withAccountStore(withCurrentIdentity(PathDetails));
......@@ -17,22 +17,22 @@
import React from 'react';
import { SafeAreaScrollViewContainer } from 'components/SafeAreaContainer';
import { NavigationAccountProps } from 'types/props';
import { withAccountStore } from 'utils/HOC';
import { NavigationAccountIdentityProps } from 'types/props';
import { withAccountStore, withCurrentIdentity } from 'utils/HOC';
import TextInput from 'components/TextInput';
import PathCard from 'components/PathCard';
function PathManagement({
accounts,
route
}: NavigationAccountProps<'PathManagement'>): React.ReactElement {
}: NavigationAccountIdentityProps<'PathManagement'>): React.ReactElement {
const path = route.params.path ?? '';
const { currentIdentity } = accounts.state;
const pathName = currentIdentity!.meta.get(path)?.name;
const pathName = currentIdentity.meta.get(path)?.name;
return (
<SafeAreaScrollViewContainer>
<PathCard identity={currentIdentity!} path={path} />
<PathCard identity={currentIdentity} path={path} />
<TextInput
label="Display Name"
onChangeText={(name: string): Promise<void> =>
......@@ -46,4 +46,4 @@ function PathManagement({
);
}
export default withAccountStore(PathManagement);
export default withAccountStore(withCurrentIdentity(PathManagement));
......@@ -19,6 +19,8 @@ import { Text, View } from 'react-native';
import { PathDetailsView } from './PathDetails';
import { navigateToPathDerivation } from 'utils/navigationHelpers';
import { useSeedRef } from 'utils/seedRefHooks';
import { SafeAreaScrollViewContainer } from 'components/SafeAreaContainer';
import { NETWORK_LIST, UnknownNetworkKeys } from 'constants/networkSpecs';
import testIDs from 'e2e/testIDs';
......@@ -27,8 +29,8 @@ import {
isEthereumNetworkParams,
isUnknownNetworkParams
} from 'types/networkSpecsTypes';
import { NavigationAccountProps } from 'types/props';
import { withAccountStore } from 'utils/HOC';
import { NavigationAccountIdentityProps } from 'types/props';
import { withAccountStore, withCurrentIdentity } from 'utils/HOC';
import {
getPathsWithSubstrateNetworkKey,
groupPaths,
......@@ -45,7 +47,7 @@ function PathsList({
accounts,
navigation,
route
}: NavigationAccountProps<'PathsList'>): React.ReactElement {
}: NavigationAccountIdentityProps<'PathsList'>): React.ReactElement {
const networkKey = route.params.networkKey ?? UnknownNetworkKeys.UNKNOWN;
const networkParams = NETWORK_LIST[networkKey];
......@@ -60,8 +62,8 @@ function PathsList({
);
return groupPaths(listedPaths);
}, [currentIdentity, isEthereumPath, networkKey]);
const { isSeedRefValid } = useSeedRef(currentIdentity.encryptedSeed);
if (!currentIdentity) return <View />;
if (isEthereumNetworkParams(networkParams)) {
return (
<PathDetailsView
......@@ -160,14 +162,16 @@ function PathsList({
<ButtonNewDerivation
testID={testIDs.PathsList.deriveButton}
title="Derive New Account"
onPress={(): void =>
navigation.navigate('PathDerivation', {
parentPath: isUnknownNetworkPath ? '' : rootPath
})
onPress={(): Promise<void> =>
navigateToPathDerivation(
navigation,
isUnknownNetworkPath ? '' : rootPath,
isSeedRefValid
)
}
/>
</SafeAreaScrollViewContainer>
);
}
export default withAccountStore(PathsList);
export default withAccountStore(withCurrentIdentity(PathsList));
......@@ -14,6 +14,8 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import AccountsStore from 'stores/AccountsStore';
export type UnlockedAccount = {
address: string;
createdAt: number;
......@@ -104,6 +106,12 @@ export type AccountsStoreState = {
selectedKey: string;
};
type LensSet<T, R> = Omit<T, keyof R> & R;
export type AccountsStoreStateWithIdentity = LensSet<
AccountsStore,
{ state: LensSet<AccountsStoreState, { currentIdentity: Identity }> }
>;
export type PathGroup = {
paths: string[];
title: string;
......
......@@ -9,6 +9,7 @@ import { StackNavigationProp } from '@react-navigation/stack';
import AccountsStore from 'stores/AccountsStore';
import ScannerStore from 'stores/ScannerStore';
import { AccountsStoreStateWithIdentity, Identity } from 'types/identityTypes';
import { RootStackParamList } from 'types/routes';
export interface NavigationProps<ScreenName extends keyof RootStackParamList> {
......@@ -30,6 +31,18 @@ export interface NavigationAccountProps<
accounts: AccountsStore;
}
export interface NavigationAccountIdentityProps<
ScreenName extends keyof RootStackParamList
> extends NavigationProps<ScreenName> {
accounts: AccountsStoreStateWithIdentity;
}
export interface NavigationTargetIdentityProps<
ScreenName extends keyof RootStackParamList
> extends NavigationProps<ScreenName> {
targetIdentity: Identity;
}
export interface NavigationAccountScannerProps<
ScreenName extends keyof RootStackParamList
> extends NavigationAccountProps<ScreenName> {
......
......@@ -19,6 +19,7 @@ import React from 'react';
import { View } from 'react-native';
import { Subscribe } from 'unstated';
import { AccountsStoreStateWithIdentity, Identity } from 'types/identityTypes';
import { RootStackParamList } from 'types/routes';
import AccountsStore from 'stores/AccountsStore';
import RegistriesStore from 'stores/RegistriesStore';
......@@ -93,30 +94,30 @@ export function withRegistriesStore<T extends RegistriesInjectedProps>(
);
}
export function withCurrentIdentity<T extends AccountInjectedProps>(
WrappedComponent: React.ComponentType<T>
): React.ComponentType<T> {
export function withCurrentIdentity<
T extends { accounts: AccountsStoreStateWithIdentity }
>(WrappedComponent: React.ComponentType<T>): React.ComponentType<T> {
return (props): React.ReactElement => {
const { currentIdentity } = props.accounts.state;
if (!currentIdentity) return <View />;
if (currentIdentity === null) return <View />;
return <WrappedComponent {...props} />;
};
}
interface UnlockScreenProps {
accounts: AccountsStore;
route:
| RouteProp<RootStackParamList, 'PinUnlock'>
| RouteProp<RootStackParamList, 'PinUnlockWithPassword'>;
targetIdentity: Identity;
}
export function withTargetIdentity<T extends UnlockScreenProps>(
WrappedComponent: React.ComponentType<T>
): React.ComponentType<T> {
): React.ComponentType<T & { accounts: AccountsStore }> {
return (props): React.ReactElement => {
const targetIdentity =
props.route.params.identity ?? props.accounts.state.currentIdentity;
if (!targetIdentity) return <View />;
return <WrappedComponent {...props} />;
return <WrappedComponent {...props} targetIdentity={targetIdentity} />;
};
}
......@@ -196,3 +196,16 @@ export const navigateToLegacyAccountList = <
>(