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 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React from 'react';
import { StyleSheet, Text } from 'react-native';
import { StyleSheet, Text, View } from 'react-native';
import TouchableItem from './TouchableItem';
import Separator from './Separator';
import QrScannerTab from 'components/QrScannerTab';
import colors from 'styles/colors';
import fontStyles from 'styles/fontStyles';
import { ButtonListener } from 'types/props';
export default class ButtonNewDerivation extends React.PureComponent<{
export default class QRScannerAndDerivationTab extends React.PureComponent<{
onPress: ButtonListener;
title: string;
testID?: string;
derivationTestID?: string;
}> {
render(): React.ReactElement {
const { onPress, title, testID } = this.props;
const { onPress, title, derivationTestID } = this.props;
return (
<TouchableItem onPress={onPress} testID={testID} style={styles.body}>
<View style={styles.body}>
<Separator
shadow={true}
style={{ backgroundColor: 'transparent', marginVertical: 0 }}
shadowStyle={{ height: 16, marginTop: -16 }}
/>
<Text style={styles.icon}>//</Text>
<Text style={styles.textLabel}>{title}</Text>
</TouchableItem>
<View style={styles.tab}>
<QrScannerTab />
</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({
body: {
body: { flexDirection: 'row' },
derivationButton: {
alignItems: 'center',
backgroundColor: colors.background.app,
height: 68
backgroundColor: colors.background.os,
height: 72
},
icon: {
...fontStyles.i_large,
color: colors.signal.main,
marginTop: 8
},
tab: {
flex: 1,
flexGrow: 1
},
textLabel: {
...fontStyles.a_text,
color: colors.text.faded,
......
......@@ -22,6 +22,7 @@ import { Icon } from 'react-native-elements';
import ButtonIcon from './ButtonIcon';
import AccountIcon from './AccountIcon';
import testIDs from 'e2e/testIDs';
import { NETWORK_LIST } from 'constants/networkSpecs';
import fontStyles from 'styles/fontStyles';
import fonts from 'styles/fonts';
......@@ -74,6 +75,7 @@ const renderBack = (onPress?: ButtonListener): ReactNode => {
iconName="arrowleft"
iconType="antdesign"
onPress={onPress}
testID={testIDs.Main.backButton}
style={StyleSheet.flatten([baseStyles.icon, { left: 0 }])}
iconBgStyle={{ backgroundColor: 'transparent' }}
/>
......
......@@ -75,7 +75,7 @@ export const SubstrateNetworkKeys: {
'0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3',
SUBSTRATE_DEV:
'0x0d667fd278ec412cd9fccdb066f09ed5b4cfd9c9afa9eb747213acb02b1e70bc', // substrate --dev commit ac6a2a783f0e1f4a814cf2add40275730cd41be1 hosted on wss://dev-node.substrate.dev .
WESTEND: '0x4a31f96525a77959d97e267c8fc3066ca333d9ade161720e1b7de8d35ccc6bd2'
WESTEND: '0xe143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e'
});
const unknownNetworkBase: { [key: string]: UnknownNetworkParams } = {
......
......@@ -15,7 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
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 { NetworkCard } from 'components/AccountCard';
......@@ -38,10 +38,10 @@ import { NavigationAccountIdentityProps } from 'types/props';
import { alertPathDerivationError } from 'utils/alertUtils';
import { getExistedNetworkKeys, getIdentityName } from 'utils/identitiesUtils';
import {
navigateToPathDerivation,
navigateToPathDetails,
navigateToPathsList,
unlockSeedPhrase
unlockSeedPhrase,
useUnlockSeed
} from 'utils/navigationHelpers';
import { useSeedRef } from 'utils/seedRefHooks';
import QrScannerTab from 'components/QrScannerTab';
......@@ -64,7 +64,7 @@ export default function NetworkSelector({
const [shouldShowMoreNetworks, setShouldShowMoreNetworks] = useState(false);
const { identities, currentIdentity } = accounts.state;
const seedRefHooks = useSeedRef(currentIdentity.encryptedSeed);
const { unlockWithoutPassword } = useUnlockSeed(seedRefHooks.isSeedRefValid);
// catch android back button and prevent exiting the app
useFocusEffect(
React.useCallback((): any => {
......@@ -84,6 +84,12 @@ export default function NetworkSelector({
}, [shouldShowMoreNetworks])
);
const onAddCustomPath = (): Promise<void> =>
unlockWithoutPassword({
name: 'PathDerivation',
params: { parentPath: '' }
});
const sortNetworkKeys = (
[, params1]: [any, NetworkParams],
[, params2]: [any, NetworkParams]
......@@ -161,32 +167,32 @@ export default function NetworkSelector({
}
};
const renderCustomPathCard = (): React.ReactElement => (
<NetworkCard
isAdd={true}
onPress={(): Promise<void> =>
navigateToPathDerivation(navigation, '', seedRefHooks.isSeedRefValid)
}
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}
/>
);
const getListOptions = (): Partial<FlatListProps<any>> => {
if (isNew) return {};
if (shouldShowMoreNetworks) {
return {
ListHeaderComponent: (
<NetworkCard
isAdd={true}
onPress={onAddCustomPath}
testID={testIDs.Main.addCustomNetworkButton}
title="Create Custom Path"
networkColor={colors.background.app}
/>
)
};
} 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({
keyExtractor={(item: [string, NetworkParams]): string => item[0]}
renderItem={renderNetwork}
testID={testIDs.Main.chooserScreen}
ListFooterComponent={renderAddButton}
{...getListOptions()}
/>
{!shouldShowMoreNetworks && !isNew && <QrScannerTab />}
</SafeAreaViewContainer>
......
......@@ -23,10 +23,10 @@ import { getSubtitle, onPinInputChange } from 'modules/unlock/utils';
import testIDs from 'e2e/testIDs';
import ScreenHeading from 'components/ScreenHeading';
import { NavigationTargetIdentityProps } from 'types/props';
import { debounce } from 'utils/debounce';
import { withAccountStore, withTargetIdentity } from 'utils/HOC';
import { unlockIdentitySeedWithReturn } from 'utils/identitiesUtils';
import { useSeedRef } from 'utils/seedRefHooks';
import Button from 'components/Button';
function PinUnlock({
targetIdentity,
......@@ -35,8 +35,7 @@ function PinUnlock({
const [state, updateState, resetState] = usePinState();
const { createSeedRef } = useSeedRef(targetIdentity.encryptedSeed);
async function submit(): Promise<void> {
const { pin } = state;
async function submit(pin: string): Promise<void> {
if (pin.length >= 6 && targetIdentity) {
try {
if (route.params.shouldReturnSeed) {
......@@ -55,13 +54,19 @@ function PinUnlock({
resolve();
}
} catch (e) {
updateState({ pin: '', pinMismatch: true });
//TODO record error times;
updateState({ pin, pinMismatch: true });
}
} 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 (
<KeyboardAwareContainer
contentContainerStyle={{
......@@ -78,16 +83,9 @@ function PinUnlock({
autoFocus
testID={testIDs.IdentityPin.unlockPinInput}
returnKeyType="done"
onChangeText={onPinInputChange('pin', updateState)}
onSubmitEditing={submit}
onChangeText={onPinInput}
value={state.pin}
/>
<Button
title={t.doneButton.pinUnlock}
onPress={submit}
testID={testIDs.IdentityPin.unlockPinButton}
aboveKeyboard
/>
</KeyboardAwareContainer>
);
}
......
......@@ -100,7 +100,6 @@ function PinUnlockWithPassword({
title={t.doneButton.pinUnlock}
onPress={submit}
testID={testIDs.IdentityPin.unlockPinButton}
aboveKeyboard
/>
</KeyboardAwareContainer>
);
......
......@@ -17,6 +17,8 @@
import React from 'react';
import { Linking, StyleSheet, Text, View } from 'react-native';
import { version } from '../../package.json';
import colors from 'styles/colors';
import fonts from 'styles/fonts';
import CustomScrollView from 'components/CustomScrollView';
......@@ -25,7 +27,7 @@ export default class About extends React.PureComponent {
render(): React.ReactElement {
return (
<CustomScrollView contentContainerStyle={{ padding: 20 }}>
<Text style={styles.title}>PARITY SIGNER</Text>
<Text style={styles.title}>PARITY SIGNER v{version}</Text>
<View>
<Text style={styles.text}>
The Parity Signer mobile application is a secure air-gapped wallet
......
......@@ -29,8 +29,8 @@ import {
getNetworkKeyByPathId,
validateDerivedPath
} from 'utils/identitiesUtils';
import { navigateToPathsList, unlockSeedPhrase } from 'utils/navigationHelpers';
import { alertPathDerivationError } from 'utils/alertUtils';
import { unlockSeedPhrase } from 'utils/navigationHelpers';
import { alertDeriveSuccess, alertPathDerivationError } from 'utils/alertUtils';
import Separator from 'components/Separator';
import ScreenHeading from 'components/ScreenHeading';
import PathCard from 'components/PathCard';
......@@ -92,7 +92,8 @@ function PathDerivation({
keyPairsName,
password
);
navigateToPathsList(navigation, currentNetworkKey);
alertDeriveSuccess();
navigation.goBack();
} catch (error) {
alertPathDerivationError(error.message);
}
......
......@@ -18,6 +18,7 @@ import { StackNavigationProp } from '@react-navigation/stack';
import React from 'react';
import { ScrollView, StyleSheet, View } from 'react-native';
import QRScannerAndDerivationTab from 'components/QRScannerAndDerivationTab';
import { SafeAreaViewContainer } from 'components/SafeAreaContainer';
import { defaultNetworkKey, UnknownNetworkKeys } from 'constants/networkSpecs';
import testIDs from 'e2e/testIDs';
......@@ -40,15 +41,10 @@ import {
isSubstratePath
} from 'utils/identitiesUtils';
import { alertDeleteAccount, alertPathDeletionError } from 'utils/alertUtils';
import {
navigateToPathDerivation,
navigateToPathsList,
useUnlockSeed
} from 'utils/navigationHelpers';
import { navigateToPathsList, useUnlockSeed } from 'utils/navigationHelpers';
import { generateAccountId } from 'utils/account';
import { UnknownAccountWarning } from 'components/Warnings';
import { useSeedRef } from 'utils/seedRefHooks';
import QrScannerTab from 'components/QrScannerTab';
interface Props {
path: string;
......@@ -69,7 +65,9 @@ export function PathDetailsView({
const address = getAddressWithPath(path, currentIdentity);
const accountName = getPathName(path, currentIdentity);
const { isSeedRefValid } = useSeedRef(currentIdentity.encryptedSeed);
const { unlockWithoutPassword, unlockWithPassword } = useUnlockSeed();
const { unlockWithoutPassword, unlockWithPassword } = useUnlockSeed(
isSeedRefValid
);
if (!address) return <View />;
const isUnknownNetwork = networkKey === UnknownNetworkKeys.UNKNOWN;
const formattedNetworkKey = isUnknownNetwork ? defaultNetworkKey : networkKey;
......@@ -78,6 +76,12 @@ export function PathDetailsView({
networkKey: formattedNetworkKey
});
const onTapDeriveButton = (): Promise<void> =>
unlockWithoutPassword({
name: 'PathDerivation',
params: { parentPath: path }
});
const onOptionSelect = async (value: string): Promise<void> => {
switch (value) {
case 'PathDelete':
......@@ -101,30 +105,21 @@ export function PathDetailsView({
}
});
break;
case 'PathSecret': {
case 'PathExport': {
const pathMeta = currentIdentity.meta.get(path)!;
if (pathMeta.hasPassword) {
await unlockWithPassword(
password => ({
name: 'PathSecret',
params: {
password,
path
}
}),
isSeedRefValid
);
await unlockWithPassword(password => ({
name: 'PathSecret',
params: {
password,
path
}
}));
} else {
await unlockWithoutPassword(
{ name: 'PathSecret', params: { path } },
isSeedRefValid
);
await unlockWithoutPassword({ name: 'PathSecret', params: { path } });
}
break;
}
case 'PathDerivation':
navigateToPathDerivation(navigation, path, isSeedRefValid);
break;
case 'PathManagement':
navigation.navigate('PathManagement', { path });
break;
......@@ -144,16 +139,11 @@ export function PathDetailsView({
menuTriggerIconName={'more-vert'}
menuItems={[
{ text: 'Edit', value: 'PathManagement' },
{
hide: !isSubstratePath(path),
text: 'Derive Account',
value: 'PathDerivation'
},
{
hide: !isSubstrateHardDerivedPath(path),
testID: testIDs.PathDetail.exportButton,
text: 'Export Account',
value: 'PathSecret'
value: 'PathExport'
},
{
testID: testIDs.PathDetail.deleteButton,
......@@ -169,7 +159,13 @@ export function PathDetailsView({
<QrView data={`${accountId}:${accountName}`} />
{isUnknownNetwork && <UnknownAccountWarning isPath />}
</ScrollView>
<QrScannerTab />
{isSubstratePath(path) && (
<QRScannerAndDerivationTab
derivationTestID={testIDs.PathDetail.deriveButton}
title="Derive New Account"
onPress={onTapDeriveButton}
/>
)}
</SafeAreaViewContainer>
);
}
......
......@@ -54,7 +54,7 @@ function PathSecret({
}, [
path,
pathMeta,
route,
route.params.password,
navigation,
currentIdentity,
isSeedRefValid,
......
......@@ -19,7 +19,7 @@ import { ScrollView, Text, View } from 'react-native';
import { PathDetailsView } from './PathDetails';
import { navigateToPathDerivation } from 'utils/navigationHelpers';
import { useUnlockSeed } from 'utils/navigationHelpers';
import { useSeedRef } from 'utils/seedRefHooks';
import { SafeAreaViewContainer } from 'components/SafeAreaContainer';
import { NETWORK_LIST, UnknownNetworkKeys } from 'constants/networkSpecs';
......@@ -36,12 +36,11 @@ import {
groupPaths,
removeSlash
} from 'utils/identitiesUtils';
import ButtonNewDerivation from 'components/ButtonNewDerivation';
import QRScannerAndDerivationTab from 'components/QRScannerAndDerivationTab';
import PathCard from 'components/PathCard';
import Separator from 'components/Separator';
import fontStyles from 'styles/fontStyles';
import { LeftScreenHeading } from 'components/ScreenHeading';
import QrScannerTab from 'components/QrScannerTab';
function PathsList({
accounts,
......@@ -63,6 +62,7 @@ function PathsList({
return groupPaths(listedPaths);
}, [currentIdentity, isEthereumPath, networkKey]);
const { isSeedRefValid } = useSeedRef(currentIdentity.encryptedSeed);
const { unlockWithoutPassword } = useUnlockSeed(isSeedRefValid);
if (isEthereumNetworkParams(networkParams)) {
return (
......@@ -78,6 +78,12 @@ function PathsList({
const { navigate } = navigation;
const rootPath = `//${networkParams.pathId}`;
const onTapDeriveButton = (): Promise<void> =>
unlockWithoutPassword({
name: 'PathDerivation',
params: { parentPath: isUnknownNetworkPath ? '' : rootPath }
});
const renderSinglePath = (pathsGroup: PathGroup): React.ReactElement => {
const path = pathsGroup.paths[0];
return (
......@@ -139,18 +145,11 @@ function PathsList({
)}
<Separator style={{ backgroundColor: 'transparent' }} />
</ScrollView>
<ButtonNewDerivation
testID={testIDs.PathsList.deriveButton}
<QRScannerAndDerivationTab
derivationTestID={testIDs.PathsList.deriveButton}
title="Derive New Account"
onPress={(): Promise<void> =>
navigateToPathDerivation(
navigation,
isUnknownNetworkPath ? '' : rootPath,
isSeedRefValid
)
}
onPress={onTapDeriveButton}
/>
<QrScannerTab />
</SafeAreaViewContainer>
);
}
......
......@@ -164,3 +164,11 @@ export const alertBackupDone = (onPress: () => any): void =>
}
]
);
export const alertDeriveSuccess = (): void =>
Alert.alert('Success', 'New Account Successfully derived', [
{
style: 'default',
text: 'Done'
}
]);
......@@ -24,12 +24,12 @@
* @param {number} time in milliseconds
*
*
* @return {any} the debounced function
* @return {any[]} the debounced function
*/
let timeout: any;
export function debounce(fn: any, time: number): () => void {
return function debouncedFunction(...args): void {
return function debouncedFunction(...args: any[]): void {
const functionCall = (): any => fn.apply(null, ...args);
clearTimeout(timeout);
......
......@@ -54,33 +54,24 @@ export const unlockAndReturnSeed = async <
type UnlockWithPassword = (
nextRoute: (password: string) => Route,
isSeedRefValid: boolean,
identity?: Identity
) => Promise<void>;
type UnlockWithoutPassword = (
nextRoute: Route,
isSeedRefValid: boolean,
identity?: Identity
) => Promise<void>;
export const useUnlockSeed = (): {
export const useUnlockSeed = (
isSeedRefValid: boolean
): {
unlockWithPassword: UnlockWithPassword;
unlockWithoutPassword: UnlockWithoutPassword;
} => {
const currentRoutes = useNavigationState(state => state.routes) as Route[];
const navigation = useNavigation<StackNavigationProp<RootStackParamList>>();
const newRoutes: Route[] = currentRoutes
.slice(0, currentRoutes.length - 1)
.map(routeState => {
return {
name: routeState.name,
params: routeState.params
};