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 @@ ...@@ -17,7 +17,7 @@
'use strict'; 'use strict';
import React, { useState } from 'react'; 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 { withNavigation, ScrollView } from 'react-navigation';
import ButtonIcon from './ButtonIcon'; import ButtonIcon from './ButtonIcon';
...@@ -32,6 +32,7 @@ import { ...@@ -32,6 +32,7 @@ import {
resetNavigationTo, resetNavigationTo,
resetNavigationWithNetworkChooser resetNavigationWithNetworkChooser
} from '../util/navigationHelpers'; } from '../util/navigationHelpers';
import TransparentBackground from './TransparentBackground';
function IdentitiesSwitch({ navigation, accounts }) { function IdentitiesSwitch({ navigation, accounts }) {
const defaultVisible = navigation.getParam('isSwitchOpen', false); const defaultVisible = navigation.getParam('isSwitchOpen', false);
...@@ -228,71 +229,61 @@ function IdentitiesSwitch({ navigation, accounts }) { ...@@ -228,71 +229,61 @@ function IdentitiesSwitch({ navigation, accounts }) {
testID={testIDs.IdentitiesSwitch.toggleButton} testID={testIDs.IdentitiesSwitch.toggleButton}
/> />
<Modal <TransparentBackground
animationType="none" testID={testIDs.IdentitiesSwitch.modal}
visible={visible} visible={visible}
transparent={true} setVisible={setVisible}
onRequestClose={() => setVisible(false)} style={styles.container}
animationType="none"
> >
<TouchableWithoutFeedback <View style={styles.card}>
style={{ flex: 1 }} {renderCurrentIdentityCard()}
onPressIn={() => setVisible(false)} {renderIdentities()}
> {accounts.getAccounts().size > 0 && (
<View <>
testID={testIDs.IdentitiesSwitch.modal} <ButtonIcon
style={styles.container} title="Legacy Accounts"
onPress={() => setVisible(false)} onPress={onLegacyListClicked}
> iconName="solution1"
<View style={styles.card}> iconType="antdesign"
{renderCurrentIdentityCard()} iconSize={24}
{renderIdentities()} textStyle={fontStyles.t_big}
{accounts.getAccounts().size > 0 && ( style={styles.indentedButton}
<> />
<ButtonIcon <Separator />
title="Legacy Accounts" </>
onPress={onLegacyListClicked} )}
iconName="solution1"
iconType="antdesign" <ButtonIcon
iconSize={24} title="Add Identity"
textStyle={fontStyles.t_big} testID={testIDs.IdentitiesSwitch.addIdentityButton}
style={styles.indentedButton} onPress={() => closeModalAndNavigate('IdentityNew')}
/> iconName="plus"
<Separator /> iconType="antdesign"
</> iconSize={24}
)} textStyle={fontStyles.t_big}
style={styles.indentedButton}
/>
<Separator />
{__DEV__ && (
<View>
<ButtonIcon <ButtonIcon
title="Add Identity" title="Add legacy account"
testID={testIDs.IdentitiesSwitch.addIdentityButton} onPress={() => closeModalAndNavigate('AccountNew')}
onPress={() => closeModalAndNavigate('IdentityNew')}
iconName="plus" iconName="plus"
iconType="antdesign" iconType="antdesign"
iconSize={24} iconSize={24}
textStyle={fontStyles.t_big} textStyle={fontStyles.t_big}
style={styles.indentedButton} style={styles.indentedButton}
/> />
<Separator /> <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>
</View> )}
</TouchableWithoutFeedback>
</Modal> {renderSettings()}
</View>
</TransparentBackground>
</View> </View>
); );
} }
...@@ -305,8 +296,6 @@ const styles = { ...@@ -305,8 +296,6 @@ const styles = {
paddingTop: 8 paddingTop: 8
}, },
container: { container: {
backgroundColor: 'rgba(0,0,0,0.8)',
flex: 1,
justifyContent: 'center', justifyContent: 'center',
marginTop: -24, marginTop: -24,
paddingLeft: 16, 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 { ...@@ -49,9 +49,7 @@ export default class TextInput extends React.PureComponent {
renderLabel() { renderLabel() {
const { label } = this.props; const { label } = this.props;
if (!label) return; if (!label) return;
return ( return <Text style={styles.label}>{label}</Text>;
<Text style={[fontStyles.t_regular, { marginBottom: 3 }]}>{label}</Text>
);
} }
render() { render() {
...@@ -107,6 +105,10 @@ const styles = StyleSheet.create({ ...@@ -107,6 +105,10 @@ const styles = StyleSheet.create({
input_error: { input_error: {
borderBottomColor: colors.bg_alert borderBottomColor: colors.bg_alert
}, },
label: {
marginBottom: 3,
...fontStyles.t_regular
},
viewStyle: { viewStyle: {
flexDirection: 'row' 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 }) { ...@@ -29,8 +29,17 @@ export default function UnknownAccountWarning({ isPath }) {
<Text style={styles.warningText}> <Text style={styles.warningText}>
This account is not bond to a specific network. This account is not bond to a specific network.
{'\n'} {'\n'}
The address currently displayed is using Kusama format, however, the {'\n'}
address for this account may be different on another network. 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>
) : ( ) : (
<Text style={styles.warningText}> <Text style={styles.warningText}>
......
...@@ -45,17 +45,18 @@ import ScreenHeading, { IdentityHeading } from '../components/ScreenHeading'; ...@@ -45,17 +45,18 @@ import ScreenHeading, { IdentityHeading } from '../components/ScreenHeading';
import fontStyles from '../fontStyles'; import fontStyles from '../fontStyles';
import { NetworkCard } from '../components/AccountCard'; 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 }) { function AccountNetworkChooser({ navigation, accounts }) {
const isNew = navigation.getParam('isNew', false); const isNew = navigation.getParam('isNew', false);
const [shouldShowMoreNetworks, setShouldShowMoreNetworks] = useState(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 { identities, currentIdentity, loaded } = accounts.state;
const hasLegacyAccount = accounts.getAccounts().size !== 0; const hasLegacyAccount = accounts.getAccounts().size !== 0;
...@@ -154,34 +155,30 @@ function AccountNetworkChooser({ navigation, accounts }) { ...@@ -154,34 +155,30 @@ function AccountNetworkChooser({ navigation, accounts }) {
onDerivationFinished(derivationSucceed, networkKey, false); 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 = () => { const renderAddButton = () => {
if (isNew) return; if (isNew) return renderCustomPathCard();
if (!shouldShowMoreNetworks) { if (!shouldShowMoreNetworks) {
return ( return (
<> <NetworkCard
<NetworkCard isAdd={true}
isAdd={true} onPress={() => setShouldShowMoreNetworks(true)}
onPress={() => setShouldShowMoreNetworks(true)} testID={testIDs.AccountNetworkChooser.addNewNetworkButton}
testID={testIDs.AccountNetworkChooser.addNewNetworkButton} title="Add Network Account"
title="Add Network Account" networkColor={colors.bg}
networkColor={colors.bg} />
/>
</>
); );
} else { } else {
return ( return renderCustomPathCard();
<>
<NetworkCard
isAdd={true}
onPress={() =>
navigation.navigate('PathDerivation', { parentPath: '' })
}
testID={testIDs.AccountNetworkChooser.addCustomNetworkButton}
title="Create Custom Path"
networkColor={colors.bg}
/>
</>
);
} }
}; };
......
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
'use strict'; 'use strict';
import React, { useRef, useState } from 'react'; import React, { useRef, useState, useMemo } from 'react';
import { withNavigation } from 'react-navigation'; import { withNavigation } from 'react-navigation';
import { withAccountStore } from '../util/HOC'; import { withAccountStore } from '../util/HOC';
import { Platform, StyleSheet, Text, View } from 'react-native'; import { Platform, StyleSheet, Text, View } from 'react-native';
...@@ -37,15 +37,24 @@ import ScreenHeading from '../components/ScreenHeading'; ...@@ -37,15 +37,24 @@ import ScreenHeading from '../components/ScreenHeading';
import colors from '../colors'; import colors from '../colors';
import PathCard from '../components/PathCard'; import PathCard from '../components/PathCard';
import KeyboardScrollView from '../components/KeyboardScrollView'; import KeyboardScrollView from '../components/KeyboardScrollView';
import { defaultNetworkKey, UnknownNetworkKeys } from '../constants';
import { NetworkSelector, NetworkOptions } from '../components/NetworkSelector';
function PathDerivation({ accounts, navigation }) { function PathDerivation({ accounts, navigation }) {
const [derivationPath, setDerivationPath] = useState(''); const [derivationPath, setDerivationPath] = useState('');
const [keyPairsName, setKeyPairsName] = useState(''); const [keyPairsName, setKeyPairsName] = useState('');
const [isPathValid, setIsPathValid] = useState(true); const [isPathValid, setIsPathValid] = useState(true);
const [modalVisible, setModalVisible] = useState(false);
const [customNetworkKey, setCustomNetworkKey] = useState(defaultNetworkKey);
const pathNameInput = useRef(null);