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

feat: enable network selection for custom path (#536)

* extract root as normal custom account

* extract network root account into list

* do not create root account when create new identity

* renaming new root account

* enable derive path in paths list

* init network selector component

* complete UI component and path derivation

* complete functionality

* fix bug

* exclude network options in dev

* use pathId to index custom path account's network

* update warning

* enable create custom path and fix touchable error in android
parent 71372268
Pipeline #77712 failed with stages
in 4 minutes and 45 seconds
......@@ -17,7 +17,7 @@
'use strict';
import React, { useState } from 'react';
import { FlatList, Modal, View, TouchableWithoutFeedback } from 'react-native';
import { FlatList, View } from 'react-native';
import { withNavigation, ScrollView } from 'react-navigation';
import ButtonIcon from './ButtonIcon';
......@@ -32,6 +32,7 @@ import {
resetNavigationTo,
resetNavigationWithNetworkChooser
} from '../util/navigationHelpers';
import TransparentBackground from './TransparentBackground';
function IdentitiesSwitch({ navigation, accounts }) {
const defaultVisible = navigation.getParam('isSwitchOpen', false);
......@@ -228,20 +229,12 @@ function IdentitiesSwitch({ navigation, accounts }) {
testID={testIDs.IdentitiesSwitch.toggleButton}
/>
<Modal
animationType="none"
visible={visible}
transparent={true}
onRequestClose={() => setVisible(false)}
>
<TouchableWithoutFeedback
style={{ flex: 1 }}
onPressIn={() => setVisible(false)}
>
<View
<TransparentBackground
testID={testIDs.IdentitiesSwitch.modal}
visible={visible}
setVisible={setVisible}
style={styles.container}
onPress={() => setVisible(false)}
animationType="none"
>
<View style={styles.card}>
{renderCurrentIdentityCard()}
......@@ -290,9 +283,7 @@ function IdentitiesSwitch({ navigation, accounts }) {
{renderSettings()}
</View>
</View>
</TouchableWithoutFeedback>
</Modal>
</TransparentBackground>
</View>
);
}
......@@ -305,8 +296,6 @@ const styles = {
paddingTop: 8
},
container: {
backgroundColor: 'rgba(0,0,0,0.8)',
flex: 1,
justifyContent: 'center',
marginTop: -24,
paddingLeft: 16,
......
// 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/>.
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import {
Image,
Platform,
StyleSheet,
Text,
TouchableNativeFeedback,
TouchableOpacity,
View
} from 'react-native';
import fontStyles from '../fontStyles';
import { SUBSTRATE_NETWORK_LIST, SubstrateNetworkKeys } from '../constants';
import Icon from 'react-native-vector-icons/MaterialIcons';
import colors from '../colors';
import TransparentBackground from './TransparentBackground';
import fonts from '../fonts';
const ACCOUNT_NETWORK = 'Account Network';
const Touchable =
Platform.OS === 'android' ? TouchableNativeFeedback : TouchableOpacity;
NetworkSelector.protoTypes = {
networkKey: PropTypes.string.isRequired,
setVisible: PropTypes.func.isRequired
};
const excludedNetworks = [SubstrateNetworkKeys.KUSAMA_CC2];
if (!__DEV__) {
excludedNetworks.push(SubstrateNetworkKeys.SUBSTRATE_DEV);
excludedNetworks.push(SubstrateNetworkKeys.KUSAMA_DEV);
}
export function NetworkSelector({ networkKey, setVisible }) {
return (
<View style={styles.body}>
<Text style={styles.label}>{ACCOUNT_NETWORK}</Text>
<Touchable onPress={() => setVisible(true)}>
<View style={styles.triggerWrapper}>
<Text style={styles.triggerLabel}>
{SUBSTRATE_NETWORK_LIST[networkKey].title}
</Text>
<Icon name="more-vert" size={25} color={colors.bg_text} />
</View>
</Touchable>
</View>
);
}
NetworkOptions.propTypes = {
setNetworkKey: PropTypes.func.isRequired,
setVisible: PropTypes.func.isRequired,
visible: PropTypes.bool.isRequired
};
export function NetworkOptions({ setNetworkKey, visible, setVisible }) {
const onNetworkSelected = networkKey => {
setNetworkKey(networkKey);
setVisible(false);
};
const menuOptions = Object.entries(SUBSTRATE_NETWORK_LIST)
.filter(([networkKey]) => !excludedNetworks.includes(networkKey))
.map(([networkKey, networkParams]) => {
return (
<Touchable
key={networkKey}
value={networkKey}
onPress={() => onNetworkSelected(networkKey)}
>
<View style={styles.optionWrapper}>
<Image source={networkParams.logo} style={styles.optionLogo} />
<Text style={styles.optionText}>{networkParams.title}</Text>
</View>
</Touchable>
);
});
return (
<TransparentBackground
style={styles.optionsWrapper}
visible={visible}
setVisible={setVisible}
animationType="fade"
>
<View style={styles.optionsBackground}>
<View style={{ ...styles.optionWrapper, borderTopWidth: 0 }}>
<Text style={styles.optionHeadingText}>
{ACCOUNT_NETWORK.toUpperCase()}
</Text>
</View>
{menuOptions}
</View>
</TransparentBackground>
);
}
const styles = StyleSheet.create({
body: {
flex: 1,
marginVertical: 8,
paddingHorizontal: 16
},
label: {
flex: 1,
marginBottom: 3,
...fontStyles.t_regular
},
menuOption: {
width: '100%'
},
optionHeadingText: {
color: colors.bg_text,
fontFamily: fonts.robotoMedium,
fontSize: 14,
paddingLeft: 16
},
optionLogo: {
alignItems: 'center',
backgroundColor: colors.bg_text_sec,
borderRadius: 30,
height: 30,
justifyContent: 'center',
marginHorizontal: 16,
width: 30
},
optionText: {
color: colors.bg_text,
...fontStyles.h2
},
optionWrapper: {
alignItems: 'center',
borderTopColor: 'black',
borderTopWidth: 1,
flexDirection: 'row',
paddingVertical: 8
},
optionsBackground: {
backgroundColor: colors.bg
},
optionsWrapper: {
justifyContent: 'flex-end'
},
triggerLabel: {
flex: 1,
...fontStyles.h2
},
triggerWrapper: {
alignItems: 'center',
backgroundColor: colors.bg,
borderBottomColor: colors.card_bg_text_sec,
borderBottomWidth: 0.8,
flex: 1,
flexDirection: 'row',
height: 40,
paddingTop: 8
}
});
......@@ -49,9 +49,7 @@ export default class TextInput extends React.PureComponent {
renderLabel() {
const { label } = this.props;
if (!label) return;
return (
<Text style={[fontStyles.t_regular, { marginBottom: 3 }]}>{label}</Text>
);
return <Text style={styles.label}>{label}</Text>;
}
render() {
......@@ -107,6 +105,10 @@ const styles = StyleSheet.create({
input_error: {
borderBottomColor: colors.bg_alert
},
label: {
marginBottom: 3,
...fontStyles.t_regular
},
viewStyle: {
flexDirection: 'row'
}
......
// 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/>.
'use strict';
import React from 'react';
import PropTypes from 'prop-types';
import {
Modal,
StyleSheet,
TouchableWithoutFeedback,
View
} from 'react-native';
TransparentBackground.propTypes = {
animationType: PropTypes.string.isRequired,
setVisible: PropTypes.func.isRequired,
style: PropTypes.object,
testID: PropTypes.string,
visible: PropTypes.bool.isRequired
};
export default function TransparentBackground({
children,
visible,
setVisible,
testID,
style,
animationType
}) {
return (
<Modal
animationType={animationType}
visible={visible}
transparent={true}
onRequestClose={() => setVisible(false)}
>
<TouchableWithoutFeedback
style={{ flex: 1 }}
onPressIn={() => setVisible(false)}
>
<View
testID={testID}
style={[styles.container, style]}
onPress={() => setVisible(false)}
>
{children}
</View>
</TouchableWithoutFeedback>
</Modal>
);
}
const styles = StyleSheet.create({
container: {
backgroundColor: 'rgba(0,0,0,0.8)',
flex: 1
}
});
......@@ -29,8 +29,17 @@ export default function UnknownAccountWarning({ isPath }) {
<Text style={styles.warningText}>
This account is not bond to a specific network.
{'\n'}
The address currently displayed is using Kusama format, however, the
address for this account may be different on another network.
{'\n'}
This could be because the network specifications are updated or the
account is generated in a previous version. The address currently
displayed is using Kusama format.
{'\n'}
{'\n'}
To bind the desired network with this account you need to:
{'\n'}- remember account path
{'\n'}- delete the account
{'\n'}- tap "Add Network Account -> Create Custom Path"
{'\n'}- derive an account with the same path as before
</Text>
) : (
<Text style={styles.warningText}>
......
......@@ -45,17 +45,18 @@ import ScreenHeading, { IdentityHeading } from '../components/ScreenHeading';
import fontStyles from '../fontStyles';
import { NetworkCard } from '../components/AccountCard';
function AccountNetworkChooser({ navigation, accounts }) {
const isNew = navigation.getParam('isNew', false);
const [shouldShowMoreNetworks, setShouldShowMoreNetworks] = useState(false);
const excludedNetworks = [
const excludedNetworks = [
UnknownNetworkKeys.UNKNOWN,
SubstrateNetworkKeys.KUSAMA_CC2
];
if (!__DEV__) {
];
if (!__DEV__) {
excludedNetworks.push(SubstrateNetworkKeys.SUBSTRATE_DEV);
excludedNetworks.push(SubstrateNetworkKeys.KUSAMA_DEV);
}
}
function AccountNetworkChooser({ navigation, accounts }) {
const isNew = navigation.getParam('isNew', false);
const [shouldShowMoreNetworks, setShouldShowMoreNetworks] = useState(false);
const { identities, currentIdentity, loaded } = accounts.state;
const hasLegacyAccount = accounts.getAccounts().size !== 0;
......@@ -154,11 +155,20 @@ function AccountNetworkChooser({ navigation, accounts }) {
onDerivationFinished(derivationSucceed, networkKey, false);
};
const renderCustomPathCard = () => (
<NetworkCard
isAdd={true}
onPress={() => navigation.navigate('PathDerivation', { parentPath: '' })}
testID={testIDs.AccountNetworkChooser.addCustomNetworkButton}
title="Create Custom Path"
networkColor={colors.bg}
/>
);
const renderAddButton = () => {
if (isNew) return;
if (isNew) return renderCustomPathCard();
if (!shouldShowMoreNetworks) {
return (
<>
<NetworkCard
isAdd={true}
onPress={() => setShouldShowMoreNetworks(true)}
......@@ -166,22 +176,9 @@ function AccountNetworkChooser({ navigation, accounts }) {
title="Add Network Account"
networkColor={colors.bg}
/>
</>
);
} else {
return (
<>
<NetworkCard
isAdd={true}
onPress={() =>
navigation.navigate('PathDerivation', { parentPath: '' })
}
testID={testIDs.AccountNetworkChooser.addCustomNetworkButton}
title="Create Custom Path"
networkColor={colors.bg}
/>
</>
);
return renderCustomPathCard();
}
};
......
......@@ -16,7 +16,7 @@
'use strict';
import React, { useRef, useState } from 'react';
import React, { useRef, useState, useMemo } from 'react';
import { withNavigation } from 'react-navigation';
import { withAccountStore } from '../util/HOC';
import { Platform, StyleSheet, Text, View } from 'react-native';
......@@ -37,15 +37,24 @@ import ScreenHeading from '../components/ScreenHeading';
import colors from '../colors';
import PathCard from '../components/PathCard';
import KeyboardScrollView from '../components/KeyboardScrollView';
import { defaultNetworkKey, UnknownNetworkKeys } from '../constants';
import { NetworkSelector, NetworkOptions } from '../components/NetworkSelector';
function PathDerivation({ accounts, navigation }) {
const [derivationPath, setDerivationPath] = useState('');
const [keyPairsName, setKeyPairsName] = useState('');
const [isPathValid, setIsPathValid] = useState(true);
const [modalVisible, setModalVisible] = useState(false);
const [customNetworkKey, setCustomNetworkKey] = useState(defaultNetworkKey);
const pathNameInput = useRef(null);
const parentPath = navigation.getParam('parentPath');
const completePath = `${parentPath}${derivationPath}`;
const networkKey = getNetworkKeyByPath(completePath);
const pathIndicatedNetworkKey = useMemo(
() => getNetworkKeyByPath(completePath),
[completePath]
);
const isCustomNetwork =
pathIndicatedNetworkKey === UnknownNetworkKeys.UNKNOWN;
const onPathDerivation = async () => {
if (!validateDerivedPath(derivationPath)) {
......@@ -55,11 +64,11 @@ function PathDerivation({ accounts, navigation }) {
const derivationSucceed = await accounts.deriveNewPath(
completePath,
seedPhrase,
networkKey,
isCustomNetwork ? customNetworkKey : pathIndicatedNetworkKey,
keyPairsName
);
if (derivationSucceed) {
navigateToPathsList(navigation, networkKey);
navigateToPathsList(navigation, pathIndicatedNetworkKey);
} else {
setIsPathValid(false);
alertPathDerivationError();
......@@ -97,12 +106,19 @@ function PathDerivation({ accounts, navigation }) {
testID={testIDs.PathDerivation.nameInput}
value={keyPairsName}
/>
{isCustomNetwork && (
<NetworkSelector
networkKey={customNetworkKey}
setVisible={setModalVisible}
/>
)}
<Separator style={{ height: 0 }} />
<PathCard
identity={accounts.state.currentIdentity}
name={keyPairsName}
path={completePath}
/>
<ButtonMainAction
disabled={!validateDerivedPath(derivationPath)}
bottom={false}
......@@ -112,6 +128,13 @@ function PathDerivation({ accounts, navigation }) {
onPress={onPathDerivation}
/>
</KeyboardScrollView>
{isCustomNetwork && (
<NetworkOptions
setNetworkKey={setCustomNetworkKey}
visible={modalVisible}
setVisible={setModalVisible}
/>
)}
</View>
);
}
......
......@@ -27,8 +27,7 @@ import colors from '../colors';
import QrView from '../components/QrView';
import {
getAddressWithPath,
getNetworkKeyByPath,
getPathName,
getNetworkKey,
getPathsWithSubstrateNetwork,
isSubstratePath
} from '../util/identitiesUtils';
......@@ -41,19 +40,17 @@ import {
import testIDs from '../../e2e/testIDs';
import { generateAccountId } from '../util/account';
import UnknownAccountWarning from '../components/UnknownAccountWarning';
import AccountCard from '../components/AccountCard';
export function PathDetailsView({ accounts, navigation, path, networkKey }) {
const { currentIdentity } = accounts.state;
const address = getAddressWithPath(path, currentIdentity);
if (!address) return null;
const isUnknownNetwork = networkKey === UnknownNetworkKeys.UNKNOWN;
const formattedNetworkKey = isUnknownNetwork ? defaultNetworkKey : networkKey;
const accountId = generateAccountId({
address,
networkKey: getNetworkKeyByPath(path)
networkKey: formattedNetworkKey
});
const isUnknownNetwork = networkKey === UnknownNetworkKeys.UNKNOWN;
//TODO enable user to select networkKey.
const formattedNetworkKey = isUnknownNetwork ? defaultNetworkKey : networkKey;
const onOptionSelect = value => {
switch (value) {
......@@ -106,22 +103,9 @@ export function PathDetailsView({ accounts, navigation, path, networkKey }) {
/>
</View>
<ScrollView>
{isUnknownNetwork ? (
<>
<AccountCard
title={getPathName(path, currentIdentity)}
address={address}
networkKey={formattedNetworkKey}
/>
<QrView data={generateAccountId({ address, networkKey })} />
<UnknownAccountWarning isPath />
</>
) : (
<>
<PathCard identity={currentIdentity} path={path} />
<QrView data={accountId} />
</>
)}
{isUnknownNetwork && <UnknownAccountWarning isPath />}
</ScrollView>
</View>
);
......@@ -129,7 +113,7 @@ export function PathDetailsView({ accounts, navigation, path, networkKey }) {
function PathDetails({ accounts, navigation }) {
const path = navigation.getParam('path', '');
const networkKey = getNetworkKeyByPath(path);
const networkKey = getNetworkKey(path, accounts.state.currentIdentity);
return (
<PathDetailsView
accounts={accounts}
......
......@@ -45,7 +45,7 @@ import {
emptyIdentity,
extractAddressFromAccountId,
getAddressKeyByPath,
getNetworkKeyByPath,
getNetworkKey,
isEthereumAccountId
} from '../util/identitiesUtils';
......@@ -242,13 +242,14 @@ export default class AccountsStore extends Container<AccountsStoreState> {
getAccountFromIdentity(accountIdOrAddress) {
const isAccountId = accountIdOrAddress.split(':').length > 1;
let targetPath = null;
let targetIdentity = null;
let targetAccountId = null;
let targetIdentity = null;
let targetNetworkKey = null;
let targetPath = null;
for (const identity of this.state.identities) {
const searchList = Array.from(identity.addresses.entries());
for (const [addressKey, path] of searchList) {
const networkKey = getNetworkKeyByPath(path);
const networkKey = getNetworkKey(path, identity);
let accountId, address;
if (isEthereumAccountId(addressKey)) {
accountId = addressKey;
......@@ -266,6 +267,7 @@ export default class AccountsStore extends Container<AccountsStoreState> {
targetPath = path;