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,71 +229,61 @@ function IdentitiesSwitch({ navigation, accounts }) {
testID={testIDs.IdentitiesSwitch.toggleButton}
/>
<Modal
animationType="none"
<TransparentBackground
testID={testIDs.IdentitiesSwitch.modal}
visible={visible}
transparent={true}
onRequestClose={() => setVisible(false)}
setVisible={setVisible}
style={styles.container}
animationType="none"
>
<TouchableWithoutFeedback
style={{ flex: 1 }}
onPressIn={() => setVisible(false)}
>
<View
testID={testIDs.IdentitiesSwitch.modal}
style={styles.container}
onPress={() => setVisible(false)}
>
<View style={styles.card}>
{renderCurrentIdentityCard()}
{renderIdentities()}
{accounts.getAccounts().size > 0 && (
<>
<ButtonIcon
title="Legacy Accounts"
onPress={onLegacyListClicked}
iconName="solution1"
iconType="antdesign"
iconSize={24}
textStyle={fontStyles.t_big}
style={styles.indentedButton}
/>
<Separator />
</>
)}
<View style={styles.card}>
{renderCurrentIdentityCard()}
{renderIdentities()}
{accounts.getAccounts().size > 0 && (
<>
<ButtonIcon
title="Legacy Accounts"
onPress={onLegacyListClicked}
iconName="solution1"
iconType="antdesign"
iconSize={24}
textStyle={fontStyles.t_big}
style={styles.indentedButton}
/>
<Separator />
</>
)}
<ButtonIcon
title="Add Identity"
testID={testIDs.IdentitiesSwitch.addIdentityButton}
onPress={() => closeModalAndNavigate('IdentityNew')}
iconName="plus"
iconType="antdesign"
iconSize={24}
textStyle={fontStyles.t_big}
style={styles.indentedButton}
/>
<Separator />
{__DEV__ && (
<View>
<ButtonIcon
title="Add Identity"
testID={testIDs.IdentitiesSwitch.addIdentityButton}
onPress={() => closeModalAndNavigate('IdentityNew')}
title="Add legacy account"
onPress={() => closeModalAndNavigate('AccountNew')}
iconName="plus"
iconType="antdesign"
iconSize={24}
textStyle={fontStyles.t_big}
style={styles.indentedButton}
/>
<Separator />
{__DEV__ && (
<View>
<ButtonIcon
title="Add legacy account"
onPress={() => closeModalAndNavigate('AccountNew')}
iconName="plus"
iconType="antdesign"
iconSize={24}
textStyle={fontStyles.t_big}
style={styles.indentedButton}
/>
<Separator />
</View>
)}
{renderSettings()}
</View>
</View>
</TouchableWithoutFeedback>
</Modal>
)}
{renderSettings()}
</View>
</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';
const excludedNetworks = [
UnknownNetworkKeys.UNKNOWN,
SubstrateNetworkKeys.KUSAMA_CC2
];
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 excludedNetworks = [
UnknownNetworkKeys.UNKNOWN,
SubstrateNetworkKeys.KUSAMA_CC2
];
if (!__DEV__) {
excludedNetworks.push(SubstrateNetworkKeys.SUBSTRATE_DEV);
excludedNetworks.push(SubstrateNetworkKeys.KUSAMA_DEV);
}
const { identities, currentIdentity, loaded } = accounts.state;
const hasLegacyAccount = accounts.getAccounts().size !== 0;
......@@ -154,34 +155,30 @@ 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)}
testID={testIDs.AccountNetworkChooser.addNewNetworkButton}
title="Add Network Account"
networkColor={colors.bg}
/>
</>
<NetworkCard
isAdd={true}
onPress={() => setShouldShowMoreNetworks(true)}
testID={testIDs.AccountNetworkChooser.addNewNetworkButton}
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,