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

feat: enable create root account under a network (#494)

* add network icon to legacy account

* refactor screen heading

* fix stylesheet creation

* remove protocol from AccountIcon

* add root account derivation

* enable derivation root from derivationPath screen

* add '//kusama' into test suites

* fix match and remove console.log

* flatten styles in screenHeadings

* change workflow

* move add network button to bottom

* revert the channge on AccountNetworkChooser.js
parent c5865128
Pipeline #71714 passed with stages
in 18 minutes and 44 seconds
......@@ -36,6 +36,7 @@ const addressFunding1 = 'address1',
addressStaking = 'address4',
addressEthereum = 'address6',
addressDefault = 'addressDefault',
addressKusamaRoot = 'addressRoot',
paths = [
'//kusama//default',
'//kusama//funding/1',
......@@ -43,7 +44,8 @@ const addressFunding1 = 'address1',
'//kusama//funding/2',
'//kusama//staking/1',
'//polkadot//default',
'1'
'1',
'//kusama'
],
metaDefault = {
address: addressDefault,
......@@ -81,6 +83,12 @@ const addressFunding1 = 'address1',
name: 'Eth account',
updatedAt: 1573142786972
},
metaKusamaRoot = {
address: addressKusamaRoot,
createdAt: 1573142786972,
name: '',
updatedAt: 1573142786972
},
metaSoftKey = {
address: addressSoft,
createdAt: 1573142786972,
......@@ -94,7 +102,8 @@ const addressesMap = new Map([
[addressFunding2, paths[3]],
[addressStaking, paths[4]],
[addressPolkadot, paths[5]],
[addressEthereum, paths[6]]
[addressEthereum, paths[6]],
[addressKusamaRoot, paths[7]]
]);
const metaMap = new Map([
[paths[0], metaDefault],
......@@ -103,7 +112,8 @@ const metaMap = new Map([
[paths[3], metaStaking],
[paths[4], metaFunding2],
[paths[5], metaPolkadot],
[paths[6], metaEthereum]
[paths[6], metaEthereum],
[paths[7], metaKusamaRoot]
]);
const testIdentities = [
{
......@@ -130,7 +140,8 @@ describe('IdentitiesUtils', () => {
});
it('regroup the paths', () => {
const kusamaPaths = paths.slice(0, 5);
const kusamaPaths = paths.slice();
kusamaPaths.splice(5, 2);
const groupResult = groupPaths(kusamaPaths);
expect(groupResult).toEqual([
{
......@@ -160,7 +171,8 @@ describe('IdentitiesUtils', () => {
'funding2',
'staking1',
'PolkadotFirst',
'Eth account'
'Eth account',
''
];
paths.forEach((path, index) => {
const name = getPathName(path, testIdentities[0]);
......
......@@ -88,12 +88,7 @@ export function NetworkCard({
<Icon name="add" color={colors.bg_text} size={32} />
</View>
) : (
<AccountIcon
address={''}
protocol={network.protocol}
network={network}
style={styles.icon}
/>
<AccountIcon address={''} network={network} style={styles.icon} />
)}
<View style={styles.desc}>
<AccountPrefixedTitle title={title} />
......@@ -145,12 +140,7 @@ export default function AccountCard({
>
<CardSeparator />
<View style={[styles.content, style]}>
<AccountIcon
address={address}
protocol={network.protocol}
network={network}
style={styles.icon}
/>
<AccountIcon address={address} network={network} style={styles.icon} />
<View style={styles.desc}>
<View>
<Text style={[fontStyles.t_regular, { color: colors.bg_text_sec }]}>
......
......@@ -30,12 +30,13 @@ import { blockiesIcon } from '../util/native';
export default function AccountIcon(props) {
AccountIcon.propTypes = {
address: PropTypes.string.isRequired,
network: PropTypes.object,
protocol: PropTypes.string.isRequired
network: PropTypes.object.isRequired,
style: PropTypes.object
};
const { address, protocol, style, network } = props;
const { address, style, network } = props;
const [ethereumIconUri, setEthereumIconUri] = useState('');
const protocol = network.protocol;
useEffect(() => {
const loadEthereumIcon = function(ethereumAddress) {
......@@ -53,7 +54,7 @@ export default function AccountIcon(props) {
if (address === '') {
return (
<View>
<View style={style}>
{network.logo ? (
<Image source={network.logo} style={styles.logo} />
) : (
......
......@@ -116,11 +116,7 @@ export default class AccountIconChooser extends React.PureComponent {
};
renderIcon = ({ item, index }) => {
const {
onSelect,
network: { protocol },
value
} = this.props;
const { onSelect, network, value } = this.props;
const { address, bip39, seed } = item;
const isSelected = address.toLowerCase() === value.toLowerCase();
......@@ -137,11 +133,7 @@ export default class AccountIconChooser extends React.PureComponent {
onSelect({ isBip39: bip39, newAddress: address, newSeed: seed })
}
>
<AccountIcon
address={address}
protocol={protocol}
style={styles.icon}
/>
<AccountIcon address={address} network={network} style={styles.icon} />
</TouchableOpacity>
);
};
......
......@@ -69,12 +69,7 @@ export default function PathCard({
}}
/>
<View style={styles.content}>
<AccountIcon
address={address}
protocol={network.protocol}
network={network}
style={styles.icon}
/>
<AccountIcon address={address} network={network} style={styles.icon} />
<View style={styles.desc}>
<View>
<Text style={[fontStyles.t_regular, { color: colors.bg_text_sec }]}>
......@@ -106,7 +101,6 @@ export default function PathCard({
<View style={[styles.content, styles.contentDer]}>
<AccountIcon
address={address}
protocol={network.protocol}
network={network}
style={styles.icon}
/>
......
......@@ -20,27 +20,115 @@
import PropTypes from 'prop-types';
import React from 'react';
import { View, Text } from 'react-native';
import { View, StyleSheet, Text } from 'react-native';
import AntIcon from 'react-native-vector-icons/AntDesign';
import fontStyles from '../fontStyles';
import fonts from '../fonts';
import ButtonIcon from './ButtonIcon';
import { Icon } from 'react-native-elements';
import colors from '../colors';
import AccountIcon from './AccountIcon';
import { NETWORK_LIST } from '../constants';
import TouchableItem from './TouchableItem';
const composeStyle = StyleSheet.compose;
const renderSubtitle = (subtitle, subtitleIcon, isAlignLeft, isError) => {
if (!subtitle) return;
let subtitleBodyStyle = [baseStyles.subtitleBody],
subtitleTextStyle = [fontStyles.t_codeS];
if (isAlignLeft) {
subtitleBodyStyle.push({ justifyContent: 'flex-start' });
subtitleTextStyle.push({ textAlign: 'left' });
}
if (isError) {
subtitleTextStyle.push(baseStyles.t_error);
}
return (
<View style={subtitleBodyStyle}>
{renderSubtitleIcon(subtitleIcon)}
<Text style={subtitleTextStyle}>{subtitle}</Text>
</View>
);
};
const renderSubtitleIcon = subtitleIcon => {
if (!subtitleIcon) return;
return <AntIcon name="user" size={10} color={colors.bg_text_sec} />;
};
const renderBack = onPress => {
if (!onPress) return;
return (
<ButtonIcon
iconName="arrowleft"
iconType="antdesign"
onPress={onPress}
style={[baseStyles.icon, { left: 0, top: -8 }]}
iconBgStyle={{ backgroundColor: 'transparent' }}
/>
);
};
const renderIcon = (iconName, iconType) => {
if (!iconName) return;
return (
<View style={[baseStyles.icon, { paddingLeft: 16 }]}>
<Icon name={iconName} type={iconType} color={colors.bg_text} />
</View>
);
};
export function PathCardHeading({ title, networkKey }) {
const titleStyle = composeStyle(
fontStyles.h2,
baseStyles.t_left,
baseStyles.t_normal
);
return (
<View style={baseStyles.bodyWithIcon}>
<AccountIcon
address={''}
network={NETWORK_LIST[networkKey]}
style={baseStyles.networkIcon}
/>
<View>
<Text style={titleStyle}>{title}</Text>
</View>
</View>
);
}
export function PathListHeading({
title,
subtitle,
subtitleIcon,
networkKey,
onPress
}) {
return (
<TouchableItem style={baseStyles.bodyWithIcon} onPress={onPress}>
<AccountIcon
address={''}
network={NETWORK_LIST[networkKey]}
style={baseStyles.networkIcon}
/>
<View>
<Text style={[baseStyles.text, baseStyles.t_left]}>{title}</Text>
{renderSubtitle(subtitle, subtitleIcon, true)}
</View>
</TouchableItem>
);
}
export default class ScreenHeading extends React.PureComponent {
static propTypes = {
big: PropTypes.bool,
onPress: PropTypes.func,
small: PropTypes.bool,
subtitle: PropTypes.string,
title: PropTypes.string
};
render() {
const {
big,
title,
small,
subtitle,
subtitleL,
subtitleIcon,
......@@ -49,87 +137,36 @@ export default class ScreenHeading extends React.PureComponent {
iconName,
iconType
} = this.props;
const finalViewStyles = [styles.body];
const finalTextStyles = [fontStyles.h1, styles.t_center];
const finalSubtitleStyle = [fontStyles.t_codeS];
const finalSubtitleIconStyle = [styles.subtitleIcon];
if (big) {
finalViewStyles.push(styles.bodyL);
finalTextStyles.push(styles.t_left);
finalSubtitleIconStyle.push({ justifyContent: 'flex-start' });
} else if (small) {
finalViewStyles.push(styles.bodyL);
finalTextStyles.push([fontStyles.h2, styles.t_left, styles.t_normal]);
}
if (error) {
finalSubtitleStyle.push(styles.t_error);
}
if (subtitleL) {
finalSubtitleStyle.push({ textAlign: 'left' });
}
const renderSubtitle = () => {
if (!subtitle) return;
return (
<View style={finalSubtitleIconStyle}>
{renderSubtitleIcon()}
<Text style={[finalTextStyles, finalSubtitleStyle]}>{subtitle}</Text>
</View>
);
};
const renderSubtitleIcon = () => {
if (!subtitleIcon) return;
return <AntIcon name="user" size={10} color={colors.bg_text_sec} />;
};
const renderBack = () => {
if (!onPress) return;
return (
<ButtonIcon
iconName="arrowleft"
iconType="antdesign"
onPress={onPress}
style={[styles.icon, { left: 0, top: -8 }]}
iconBgStyle={{ backgroundColor: 'transparent' }}
/>
);
};
const renderIcon = () => {
if (!iconName) return;
return (
<View style={[styles.icon, { paddingLeft: 16 }]}>
<Icon name={iconName} type={iconType} color={colors.bg_text} />
</View>
);
};
return (
<View style={finalViewStyles}>
<Text style={finalTextStyles}>{title}</Text>
{renderSubtitle()}
{renderBack()}
{renderIcon()}
<View style={baseStyles.body}>
<Text style={baseStyles.text}>{title}</Text>
{renderSubtitle(subtitle, subtitleIcon, subtitleL, error)}
{renderBack(onPress)}
{renderIcon(iconName, iconType)}
</View>
);
}
}
const styles = {
const baseStyles = StyleSheet.create({
body: {
marginBottom: 16,
paddingHorizontal: 16
},
bodyL: {
paddingLeft: 72,
paddingRight: 16
bodyWithIcon: {
alignItems: 'center',
flexDirection: 'row',
marginBottom: 16
},
icon: {
marginLeft: 5,
position: 'absolute'
},
subtitleIcon: {
networkIcon: {
paddingHorizontal: 16
},
subtitleBody: {
alignItems: 'center',
flexDirection: 'row',
justifyContent: 'center'
......@@ -145,5 +182,9 @@ const styles = {
},
t_normal: {
fontFamily: fonts.roboto
},
text: {
...fontStyles.h1,
textAlign: 'center'
}
};
});
......@@ -32,6 +32,7 @@ import {
import fontStyles from '../fontStyles';
import UnknownAccountWarning from '../components/UnknownAccountWarning';
import { withAccountStore } from '../util/HOC';
import AccountIcon from '../components/AccountIcon';
export default withAccountStore(AccountDetails);
......@@ -75,6 +76,11 @@ function AccountDetails({ accounts, navigation }) {
<ScrollView contentContainerStyle={styles.body}>
<View style={styles.bodyContent}>
<View style={styles.header}>
<AccountIcon
address={''}
network={NETWORK_LIST[account.networkKey]}
style={styles.icon}
/>
<Text style={fontStyles.h2}>Public Address</Text>
<View style={styles.menuView}>
<PopupMenu
......@@ -125,11 +131,14 @@ const styles = StyleSheet.create({
color: colors.bg_alert
},
header: {
alignItems: 'center',
flexDirection: 'row',
paddingBottom: 24,
paddingLeft: 72,
paddingRight: 19
},
icon: {
paddingHorizontal: 16
},
menuView: {
alignItems: 'flex-end',
flex: 1
......
......@@ -30,6 +30,7 @@ import {
} from '../constants';
import {
navigateToPathsList,
navigateToSubstrateRoot,
unlockSeedPhrase
} from '../util/navigationHelpers';
import { withAccountStore } from '../util/HOC';
......@@ -40,7 +41,6 @@ import {
} from '../util/identitiesUtils';
import testIDs from '../../e2e/testIDs';
import ScreenHeading from '../components/ScreenHeading';
import Separator from '../components/Separator';
import fontStyles from '../fontStyles';
import { NetworkCard } from '../components/AccountCard';
......@@ -117,25 +117,31 @@ function AccountNetworkChooser({ navigation, accounts }) {
return availableNetworks.includes(networkKey);
};
const onDerivationFinished = (derivationSucceed, networkKey) => {
const onDerivationFinished = (
derivationSucceed,
networkKey,
isSubstrateRoot
) => {
if (derivationSucceed) {
if (isSubstrateRoot) {
return navigateToSubstrateRoot(navigation, networkKey);
}
navigateToPathsList(navigation, networkKey);
} else {
alertPathDerivationError();
}
};
const deriveSubstrateDefault = async (networkKey, networkParams) => {
const { prefix, pathId } = networkParams;
const deriveSubstrateRoot = async (networkKey, networkParams) => {
const { pathId } = networkParams;
const seedPhrase = await unlockSeedPhrase(navigation);
const derivationSucceed = await accounts.deriveNewPath(
`//${pathId}//default`,
`//${pathId}`,
seedPhrase,
prefix,
networkKey,
'Default'
'Root'
);
onDerivationFinished(derivationSucceed, networkKey);
onDerivationFinished(derivationSucceed, networkKey, true);
};
const deriveEthereumAccount = async networkKey => {
......@@ -144,7 +150,7 @@ function AccountNetworkChooser({ navigation, accounts }) {
seedPhrase,
networkKey
);
onDerivationFinished(derivationSucceed, networkKey);
onDerivationFinished(derivationSucceed, networkKey, false);
};
const renderShowMoreButton = () => {
......@@ -158,7 +164,6 @@ function AccountNetworkChooser({ navigation, accounts }) {
title="Add Network Account"
networkColor={colors.bg}
/>
<Separator style={{ backgroundColor: 'transparent', height: 120 }} />
</>
);
}
......@@ -180,7 +185,7 @@ function AccountNetworkChooser({ navigation, accounts }) {
const onNetworkChosen = async (networkKey, networkParams) => {
if (isNew) {
if (networkParams.protocol === NetworkProtocols.SUBSTRATE) {
await deriveSubstrateDefault(networkKey, networkParams);
await deriveSubstrateRoot(networkKey, networkParams);
} else {
await deriveEthereumAccount(networkKey);
}
......@@ -189,9 +194,7 @@ function AccountNetworkChooser({ navigation, accounts }) {
const listedPaths = getPathsWithSubstrateNetwork(paths, networkKey);
if (networkParams.protocol === NetworkProtocols.SUBSTRATE) {
if (listedPaths.length === 0)
return navigation.navigate('PathDerivation', {
networkKey
});
return await deriveSubstrateRoot(networkKey, networkParams);
} else if (
networkParams.protocol === NetworkProtocols.ETHEREUM &&
!paths.includes(networkKey)
......
......@@ -56,7 +56,6 @@ function PathDerivation({ accounts, navigation }) {
const derivationSucceed = await accounts.deriveNewPath(
completePath,
seedPhrase,
NETWORK_LIST[networkKey].prefix,
networkKey,
keyPairsName
);
......
......@@ -22,7 +22,7 @@ import { withNavigation } from 'react-navigation';
import { ScrollView, StyleSheet, View } from 'react-native';
import PathCard from '../components/PathCard';
import PopupMenu from '../components/PopupMenu';
import ScreenHeading from '../components/ScreenHeading';
import { PathCardHeading } from '../components/ScreenHeading';
import colors from '../colors';
import QrView from '../components/QrView';
import {
......@@ -69,7 +69,7 @@ export function PathDetailsView({ accounts, navigation, path, networkKey }) {
return (
<View style={styles.body}>
<ScreenHeading small={true} title="Public Address" />
<PathCardHeading title="Public Address" networkKey={networkKey} />
<View style={styles.menuView}>
<PopupMenu
testID={testIDs.PathDetail.popupMenuButton}
......
......@@ -28,6 +28,7 @@ import { withAccountStore } from '../util/HOC';
import { withNavigation } from 'react-navigation';
import {
getPathsWithSubstrateNetwork,
getRootPathMeta,
groupPaths,
removeSlash
} from '../util/identitiesUtils';
......@@ -39,19 +40,27 @@ import testIDs from '../../e2e/testIDs';
import Separator from '../components/Separator';
import fontStyles from '../fontStyles';
import colors from '../colors';
import ScreenHeading from '../components/ScreenHeading';
import { PathListHeading } from '../components/ScreenHeading';
import {
alertDeriveRootPath,
alertPathDerivationError
} from '../util/alertUtils';
import {
navigateToPathDetails,
unlockSeedPhrase
} from '../util/navigationHelpers';
function PathsList({ accounts, navigation }) {
const networkKey = navigation.getParam(
'networkKey',
UnknownNetworkKeys.UNKNOWN
);
const networkParams = NETWORK_LIST[networkKey];
const { currentIdentity } = accounts.state;
const isEthereumPath =
NETWORK_LIST[networkKey].protocol === NetworkProtocols.ETHEREUM;
const isEthereumPath = networkParams.protocol === NetworkProtocols.ETHEREUM;
const isUnknownNetworkPath =
NETWORK_LIST[networkKey].protocol === NetworkProtocols.UNKNOWN;
networkParams.protocol === NetworkProtocols.UNKNOWN;
const pathsGroups = useMemo(() => {
if (!currentIdentity || isEthereumPath) return null;
const paths = Array.from(currentIdentity.meta.keys());
......@@ -73,6 +82,30 @@ function PathsList({ accounts, navigation }) {