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

fix: v4.3 UX according to user feedback (#643)

* move the derive button to the button

* remove unused button

* refactor redirection function

* change the redirection after derivation

* refactor e2e tests to fit new work flow

* add version in about

* validate pin input without button tapped

* Update NetworkSelector.tsx

* update westend genesis hash

* fix e2e test

* move two tabs in the bottom to one

* fix e2e test

* bump version to 4.3.2

* bump dependencies

* remove password length limitation

* update xcode settings

* fix e2e test
parent 0a0563e1
Pipeline #97967 failed with stages
in 3 minutes and 6 seconds
...@@ -15,47 +15,64 @@ ...@@ -15,47 +15,64 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React from 'react'; import React from 'react';
import { StyleSheet, Text } from 'react-native'; import { StyleSheet, Text, View } from 'react-native';
import TouchableItem from './TouchableItem'; import TouchableItem from './TouchableItem';
import Separator from './Separator'; import Separator from './Separator';
import QrScannerTab from 'components/QrScannerTab';
import colors from 'styles/colors'; import colors from 'styles/colors';
import fontStyles from 'styles/fontStyles'; import fontStyles from 'styles/fontStyles';
import { ButtonListener } from 'types/props'; import { ButtonListener } from 'types/props';
export default class ButtonNewDerivation extends React.PureComponent<{ export default class QRScannerAndDerivationTab extends React.PureComponent<{
onPress: ButtonListener; onPress: ButtonListener;
title: string; title: string;
testID?: string; derivationTestID?: string;
}> { }> {
render(): React.ReactElement { render(): React.ReactElement {
const { onPress, title, testID } = this.props; const { onPress, title, derivationTestID } = this.props;
return ( return (
<TouchableItem onPress={onPress} testID={testID} style={styles.body}> <View style={styles.body}>
<Separator <Separator
shadow={true} shadow={true}
style={{ backgroundColor: 'transparent', marginVertical: 0 }} style={{ backgroundColor: 'transparent', marginVertical: 0 }}
shadowStyle={{ height: 16, marginTop: -16 }} shadowStyle={{ height: 16, marginTop: -16 }}
/> />
<Text style={styles.icon}>//</Text> <View style={styles.tab}>
<Text style={styles.textLabel}>{title}</Text> <QrScannerTab />
</TouchableItem> </View>
<View style={styles.tab}>
<TouchableItem
onPress={onPress}
style={styles.derivationButton}
testID={derivationTestID}
>
<Text style={styles.icon}>//</Text>
<Text style={styles.textLabel}>{title}</Text>
</TouchableItem>
</View>
</View>
); );
} }
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({
body: { body: { flexDirection: 'row' },
derivationButton: {
alignItems: 'center', alignItems: 'center',
backgroundColor: colors.background.app, backgroundColor: colors.background.os,
height: 68 height: 72
}, },
icon: { icon: {
...fontStyles.i_large, ...fontStyles.i_large,
color: colors.signal.main, color: colors.signal.main,
marginTop: 8 marginTop: 8
}, },
tab: {
flex: 1,
flexGrow: 1
},
textLabel: { textLabel: {
...fontStyles.a_text, ...fontStyles.a_text,
color: colors.text.faded, color: colors.text.faded,
......
...@@ -22,6 +22,7 @@ import { Icon } from 'react-native-elements'; ...@@ -22,6 +22,7 @@ import { Icon } from 'react-native-elements';
import ButtonIcon from './ButtonIcon'; import ButtonIcon from './ButtonIcon';
import AccountIcon from './AccountIcon'; import AccountIcon from './AccountIcon';
import testIDs from 'e2e/testIDs';
import { NETWORK_LIST } from 'constants/networkSpecs'; import { NETWORK_LIST } from 'constants/networkSpecs';
import fontStyles from 'styles/fontStyles'; import fontStyles from 'styles/fontStyles';
import fonts from 'styles/fonts'; import fonts from 'styles/fonts';
...@@ -74,6 +75,7 @@ const renderBack = (onPress?: ButtonListener): ReactNode => { ...@@ -74,6 +75,7 @@ const renderBack = (onPress?: ButtonListener): ReactNode => {
iconName="arrowleft" iconName="arrowleft"
iconType="antdesign" iconType="antdesign"
onPress={onPress} onPress={onPress}
testID={testIDs.Main.backButton}
style={StyleSheet.flatten([baseStyles.icon, { left: 0 }])} style={StyleSheet.flatten([baseStyles.icon, { left: 0 }])}
iconBgStyle={{ backgroundColor: 'transparent' }} iconBgStyle={{ backgroundColor: 'transparent' }}
/> />
......
...@@ -75,7 +75,7 @@ export const SubstrateNetworkKeys: { ...@@ -75,7 +75,7 @@ export const SubstrateNetworkKeys: {
'0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3', '0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3',
SUBSTRATE_DEV: SUBSTRATE_DEV:
'0x0d667fd278ec412cd9fccdb066f09ed5b4cfd9c9afa9eb747213acb02b1e70bc', // substrate --dev commit ac6a2a783f0e1f4a814cf2add40275730cd41be1 hosted on wss://dev-node.substrate.dev . '0x0d667fd278ec412cd9fccdb066f09ed5b4cfd9c9afa9eb747213acb02b1e70bc', // substrate --dev commit ac6a2a783f0e1f4a814cf2add40275730cd41be1 hosted on wss://dev-node.substrate.dev .
WESTEND: '0x4a31f96525a77959d97e267c8fc3066ca333d9ade161720e1b7de8d35ccc6bd2' WESTEND: '0xe143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e'
}); });
const unknownNetworkBase: { [key: string]: UnknownNetworkParams } = { const unknownNetworkBase: { [key: string]: UnknownNetworkParams } = {
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { ReactElement, useState } from 'react'; import React, { ReactElement, useState } from 'react';
import { BackHandler, FlatList } from 'react-native'; import { BackHandler, FlatList, FlatListProps } from 'react-native';
import { useFocusEffect } from '@react-navigation/native'; import { useFocusEffect } from '@react-navigation/native';
import { NetworkCard } from 'components/AccountCard'; import { NetworkCard } from 'components/AccountCard';
...@@ -38,10 +38,10 @@ import { NavigationAccountIdentityProps } from 'types/props'; ...@@ -38,10 +38,10 @@ import { NavigationAccountIdentityProps } from 'types/props';
import { alertPathDerivationError } from 'utils/alertUtils'; import { alertPathDerivationError } from 'utils/alertUtils';
import { getExistedNetworkKeys, getIdentityName } from 'utils/identitiesUtils'; import { getExistedNetworkKeys, getIdentityName } from 'utils/identitiesUtils';
import { import {
navigateToPathDerivation,
navigateToPathDetails, navigateToPathDetails,
navigateToPathsList, navigateToPathsList,
unlockSeedPhrase unlockSeedPhrase,
useUnlockSeed
} from 'utils/navigationHelpers'; } from 'utils/navigationHelpers';
import { useSeedRef } from 'utils/seedRefHooks'; import { useSeedRef } from 'utils/seedRefHooks';
import QrScannerTab from 'components/QrScannerTab'; import QrScannerTab from 'components/QrScannerTab';
...@@ -64,7 +64,7 @@ export default function NetworkSelector({ ...@@ -64,7 +64,7 @@ export default function NetworkSelector({
const [shouldShowMoreNetworks, setShouldShowMoreNetworks] = useState(false); const [shouldShowMoreNetworks, setShouldShowMoreNetworks] = useState(false);
const { identities, currentIdentity } = accounts.state; const { identities, currentIdentity } = accounts.state;
const seedRefHooks = useSeedRef(currentIdentity.encryptedSeed); const seedRefHooks = useSeedRef(currentIdentity.encryptedSeed);
const { unlockWithoutPassword } = useUnlockSeed(seedRefHooks.isSeedRefValid);
// catch android back button and prevent exiting the app // catch android back button and prevent exiting the app
useFocusEffect( useFocusEffect(
React.useCallback((): any => { React.useCallback((): any => {
...@@ -84,6 +84,12 @@ export default function NetworkSelector({ ...@@ -84,6 +84,12 @@ export default function NetworkSelector({
}, [shouldShowMoreNetworks]) }, [shouldShowMoreNetworks])
); );
const onAddCustomPath = (): Promise<void> =>
unlockWithoutPassword({
name: 'PathDerivation',
params: { parentPath: '' }
});
const sortNetworkKeys = ( const sortNetworkKeys = (
[, params1]: [any, NetworkParams], [, params1]: [any, NetworkParams],
[, params2]: [any, NetworkParams] [, params2]: [any, NetworkParams]
...@@ -161,32 +167,32 @@ export default function NetworkSelector({ ...@@ -161,32 +167,32 @@ export default function NetworkSelector({
} }
}; };
const renderCustomPathCard = (): React.ReactElement => ( const getListOptions = (): Partial<FlatListProps<any>> => {
<NetworkCard if (isNew) return {};
isAdd={true} if (shouldShowMoreNetworks) {
onPress={(): Promise<void> => return {
navigateToPathDerivation(navigation, '', seedRefHooks.isSeedRefValid) ListHeaderComponent: (
} <NetworkCard
testID={testIDs.Main.addCustomNetworkButton} isAdd={true}
title="Create Custom Path" onPress={onAddCustomPath}
networkColor={colors.background.app} testID={testIDs.Main.addCustomNetworkButton}
/> title="Create Custom Path"
); networkColor={colors.background.app}
/>
const renderAddButton = (): React.ReactElement => { )
if (isNew) return renderCustomPathCard(); };
if (!shouldShowMoreNetworks) {
return (
<NetworkCard
isAdd={true}
onPress={(): void => setShouldShowMoreNetworks(true)}
testID={testIDs.Main.addNewNetworkButton}
title="Add Network Account"
networkColor={colors.background.app}
/>
);
} else { } else {
return renderCustomPathCard(); return {
ListFooterComponent: (
<NetworkCard
isAdd={true}
onPress={(): void => setShouldShowMoreNetworks(true)}
testID={testIDs.Main.addNewNetworkButton}
title="Add Network Account"
networkColor={colors.background.app}
/>
)
};
} }
}; };
...@@ -260,7 +266,7 @@ export default function NetworkSelector({ ...@@ -260,7 +266,7 @@ export default function NetworkSelector({
keyExtractor={(item: [string, NetworkParams]): string => item[0]} keyExtractor={(item: [string, NetworkParams]): string => item[0]}
renderItem={renderNetwork} renderItem={renderNetwork}
testID={testIDs.Main.chooserScreen} testID={testIDs.Main.chooserScreen}
ListFooterComponent={renderAddButton} {...getListOptions()}
/> />
{!shouldShowMoreNetworks && !isNew && <QrScannerTab />} {!shouldShowMoreNetworks && !isNew && <QrScannerTab />}
</SafeAreaViewContainer> </SafeAreaViewContainer>
......
...@@ -23,10 +23,10 @@ import { getSubtitle, onPinInputChange } from 'modules/unlock/utils'; ...@@ -23,10 +23,10 @@ import { getSubtitle, onPinInputChange } from 'modules/unlock/utils';
import testIDs from 'e2e/testIDs'; import testIDs from 'e2e/testIDs';
import ScreenHeading from 'components/ScreenHeading'; import ScreenHeading from 'components/ScreenHeading';
import { NavigationTargetIdentityProps } from 'types/props'; import { NavigationTargetIdentityProps } from 'types/props';
import { debounce } from 'utils/debounce';
import { withAccountStore, withTargetIdentity } from 'utils/HOC'; import { withAccountStore, withTargetIdentity } from 'utils/HOC';
import { unlockIdentitySeedWithReturn } from 'utils/identitiesUtils'; import { unlockIdentitySeedWithReturn } from 'utils/identitiesUtils';
import { useSeedRef } from 'utils/seedRefHooks'; import { useSeedRef } from 'utils/seedRefHooks';
import Button from 'components/Button';
function PinUnlock({ function PinUnlock({
targetIdentity, targetIdentity,
...@@ -35,8 +35,7 @@ function PinUnlock({ ...@@ -35,8 +35,7 @@ function PinUnlock({
const [state, updateState, resetState] = usePinState(); const [state, updateState, resetState] = usePinState();
const { createSeedRef } = useSeedRef(targetIdentity.encryptedSeed); const { createSeedRef } = useSeedRef(targetIdentity.encryptedSeed);
async function submit(): Promise<void> { async function submit(pin: string): Promise<void> {
const { pin } = state;
if (pin.length >= 6 && targetIdentity) { if (pin.length >= 6 && targetIdentity) {
try { try {
if (route.params.shouldReturnSeed) { if (route.params.shouldReturnSeed) {
...@@ -55,13 +54,19 @@ function PinUnlock({ ...@@ -55,13 +54,19 @@ function PinUnlock({
resolve(); resolve();
} }
} catch (e) { } catch (e) {
updateState({ pin: '', pinMismatch: true }); updateState({ pin, pinMismatch: true });
//TODO record error times;
} }
} else { } else {
updateState({ pinTooShort: true }); updateState({ pin, pinTooShort: true });
} }
} }
const onPinInput = (pin: string): void => {
onPinInputChange('pin', updateState)(pin);
const debounceSubmit = debounce(() => submit(pin), 500);
debounceSubmit();
};
return ( return (
<KeyboardAwareContainer <KeyboardAwareContainer
contentContainerStyle={{ contentContainerStyle={{
...@@ -78,16 +83,9 @@ function PinUnlock({ ...@@ -78,16 +83,9 @@ function PinUnlock({
autoFocus autoFocus
testID={testIDs.IdentityPin.unlockPinInput} testID={testIDs.IdentityPin.unlockPinInput}
returnKeyType="done" returnKeyType="done"
onChangeText={onPinInputChange('pin', updateState)} onChangeText={onPinInput}
onSubmitEditing={submit}
value={state.pin} value={state.pin}
/> />
<Button
title={t.doneButton.pinUnlock}
onPress={submit}
testID={testIDs.IdentityPin.unlockPinButton}
aboveKeyboard
/>
</KeyboardAwareContainer> </KeyboardAwareContainer>
); );
} }
......
...@@ -100,7 +100,6 @@ function PinUnlockWithPassword({ ...@@ -100,7 +100,6 @@ function PinUnlockWithPassword({
title={t.doneButton.pinUnlock} title={t.doneButton.pinUnlock}
onPress={submit} onPress={submit}
testID={testIDs.IdentityPin.unlockPinButton} testID={testIDs.IdentityPin.unlockPinButton}
aboveKeyboard
/> />
</KeyboardAwareContainer> </KeyboardAwareContainer>
); );
......
...@@ -17,6 +17,8 @@ ...@@ -17,6 +17,8 @@
import React from 'react'; import React from 'react';
import { Linking, StyleSheet, Text, View } from 'react-native'; import { Linking, StyleSheet, Text, View } from 'react-native';
import { version } from '../../package.json';
import colors from 'styles/colors'; import colors from 'styles/colors';
import fonts from 'styles/fonts'; import fonts from 'styles/fonts';
import CustomScrollView from 'components/CustomScrollView'; import CustomScrollView from 'components/CustomScrollView';
...@@ -25,7 +27,7 @@ export default class About extends React.PureComponent { ...@@ -25,7 +27,7 @@ export default class About extends React.PureComponent {
render(): React.ReactElement { render(): React.ReactElement {
return ( return (
<CustomScrollView contentContainerStyle={{ padding: 20 }}> <CustomScrollView contentContainerStyle={{ padding: 20 }}>
<Text style={styles.title}>PARITY SIGNER</Text> <Text style={styles.title}>PARITY SIGNER v{version}</Text>
<View> <View>
<Text style={styles.text}> <Text style={styles.text}>
The Parity Signer mobile application is a secure air-gapped wallet The Parity Signer mobile application is a secure air-gapped wallet
......
...@@ -29,8 +29,8 @@ import { ...@@ -29,8 +29,8 @@ import {
getNetworkKeyByPathId, getNetworkKeyByPathId,
validateDerivedPath validateDerivedPath
} from 'utils/identitiesUtils'; } from 'utils/identitiesUtils';
import { navigateToPathsList, unlockSeedPhrase } from 'utils/navigationHelpers'; import { unlockSeedPhrase } from 'utils/navigationHelpers';
import { alertPathDerivationError } from 'utils/alertUtils'; import { alertDeriveSuccess, alertPathDerivationError } from 'utils/alertUtils';
import Separator from 'components/Separator'; import Separator from 'components/Separator';
import ScreenHeading from 'components/ScreenHeading'; import ScreenHeading from 'components/ScreenHeading';
import PathCard from 'components/PathCard'; import PathCard from 'components/PathCard';
...@@ -92,7 +92,8 @@ function PathDerivation({ ...@@ -92,7 +92,8 @@ function PathDerivation({
keyPairsName, keyPairsName,
password password
); );
navigateToPathsList(navigation, currentNetworkKey); alertDeriveSuccess();
navigation.goBack();
} catch (error) { } catch (error) {
alertPathDerivationError(error.message); alertPathDerivationError(error.message);
} }
......
...@@ -18,6 +18,7 @@ import { StackNavigationProp } from '@react-navigation/stack'; ...@@ -18,6 +18,7 @@ import { StackNavigationProp } from '@react-navigation/stack';
import React from 'react'; import React from 'react';
import { ScrollView, StyleSheet, View } from 'react-native'; import { ScrollView, StyleSheet, View } from 'react-native';
import QRScannerAndDerivationTab from 'components/QRScannerAndDerivationTab';
import { SafeAreaViewContainer } from 'components/SafeAreaContainer'; import { SafeAreaViewContainer } from 'components/SafeAreaContainer';
import { defaultNetworkKey, UnknownNetworkKeys } from 'constants/networkSpecs'; import { defaultNetworkKey, UnknownNetworkKeys } from 'constants/networkSpecs';
import testIDs from 'e2e/testIDs'; import testIDs from 'e2e/testIDs';
...@@ -40,15 +41,10 @@ import { ...@@ -40,15 +41,10 @@ import {
isSubstratePath isSubstratePath
} from 'utils/identitiesUtils'; } from 'utils/identitiesUtils';
import { alertDeleteAccount, alertPathDeletionError } from 'utils/alertUtils'; import { alertDeleteAccount, alertPathDeletionError } from 'utils/alertUtils';
import { import { navigateToPathsList, useUnlockSeed } from 'utils/navigationHelpers';
navigateToPathDerivation,
navigateToPathsList,
useUnlockSeed
} from 'utils/navigationHelpers';
import { generateAccountId } from 'utils/account'; import { generateAccountId } from 'utils/account';
import { UnknownAccountWarning } from 'components/Warnings'; import { UnknownAccountWarning } from 'components/Warnings';
import { useSeedRef } from 'utils/seedRefHooks'; import { useSeedRef } from 'utils/seedRefHooks';
import QrScannerTab from 'components/QrScannerTab';
interface Props { interface Props {
path: string; path: string;
...@@ -69,7 +65,9 @@ export function PathDetailsView({ ...@@ -69,7 +65,9 @@ export function PathDetailsView({
const address = getAddressWithPath(path, currentIdentity); const address = getAddressWithPath(path, currentIdentity);
const accountName = getPathName(path, currentIdentity); const accountName = getPathName(path, currentIdentity);
const { isSeedRefValid } = useSeedRef(currentIdentity.encryptedSeed); const { isSeedRefValid } = useSeedRef(currentIdentity.encryptedSeed);
const { unlockWithoutPassword, unlockWithPassword } = useUnlockSeed(); const { unlockWithoutPassword, unlockWithPassword } = useUnlockSeed(
isSeedRefValid
);
if (!address) return <View />; if (!address) return <View />;
const isUnknownNetwork = networkKey === UnknownNetworkKeys.UNKNOWN; const isUnknownNetwork = networkKey === UnknownNetworkKeys.UNKNOWN;
const formattedNetworkKey = isUnknownNetwork ? defaultNetworkKey : networkKey; const formattedNetworkKey = isUnknownNetwork ? defaultNetworkKey : networkKey;
...@@ -78,6 +76,12 @@ export function PathDetailsView({ ...@@ -78,6 +76,12 @@ export function PathDetailsView({
networkKey: formattedNetworkKey networkKey: formattedNetworkKey
}); });
const onTapDeriveButton = (): Promise<void> =>
unlockWithoutPassword({
name: 'PathDerivation',
params: { parentPath: path }
});
const onOptionSelect = async (value: string): Promise<void> => { const onOptionSelect = async (value: string): Promise<void> => {
switch (value) { switch (value) {
case 'PathDelete': case 'PathDelete':
...@@ -101,30 +105,21 @@ export function PathDetailsView({ ...@@ -101,30 +105,21 @@ export function PathDetailsView({
} }
}); });
break; break;
case 'PathSecret': { case 'PathExport': {
const pathMeta = currentIdentity.meta.get(path)!; const pathMeta = currentIdentity.meta.get(path)!;
if (pathMeta.hasPassword) { if (pathMeta.hasPassword) {
await unlockWithPassword( await unlockWithPassword(password => ({
password => ({ name: 'PathSecret',
name: 'PathSecret', params: {
params: { password,
password, path
path }
} }));
}),
isSeedRefValid
);
} else { } else {
await unlockWithoutPassword( await unlockWithoutPassword({ name: 'PathSecret', params: { path } });
{ name: 'PathSecret', params: { path } },
isSeedRefValid
);
} }
break; break;
} }
case 'PathDerivation':
navigateToPathDerivation(navigation, path, isSeedRefValid);
break;
case