Unverified Commit 23cc5f65 authored by Hanwen Cheng's avatar Hanwen Cheng Committed by GitHub
Browse files

feat: support pinless signing when user already input pin recently (#589)

* fix: upgrade react navigation v5

* update project settings in ios

* improve navigation by upgrading to react navigation v5

* make compiler happy

* unlink react-native-secure-storage

* make detox and react-navigation happy again

* make e2e test happy

* use safe area context app wide

* delete stray comment

* fix screen heading styles

* fix pin backup navigation

* revert change to rust target

* fix ui overlap on android

* remove bounce in ios scroll view

* lint fix

* feat: enable passworded identity

* use keyboard scroll view

* complete password generation

* update react-native-camera and related polkadot api packages

* add registry store and use type override

* reduce android bundle size

* update yarn.lock

* update metadata

* prettier happy

* update polkadot api

* add password in pinInputt

* remove password from identity

* add password in path derivation

* remove log

* complete password support

* make compiler happy

* refactor account store error handling

* remove password check when signing

* add lock icon for passworded account

* add hint also on path

* add extra hint text

* fix autofocus and remove useRef

* add e2e test suit for passworded account

* make lint happy

* destroy reference when app go into background

* fix lint

* add seed ref functions

* enable pin los address creation

* signing with seed reference

* fix bridge in ios

* use lists for data pointers for each identity

* createSeedRefWithNewSeed function

* fix logic error

* more fix and complete e2e test

* remove console log

* add copyright

* fix lint errors

* fix unlock identity error
parent 1b51dd33
Pipeline #90497 failed with stages
in 3 minutes and 41 seconds
......@@ -55,7 +55,7 @@ RCT_EXTERN_METHOD(schnorrkelVerify: (NSString*)seed message:(NSString*)message s
RCT_EXTERN_METHOD(decryptDataRef:(NSString*)data password:(NSString*)password resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(destroyDataRef:(int64_t)dataRef resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(brainWalletSignWithRef:(int64_t)seedRef message:(NSString*)message resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(substrateSignWithRef:(int64_t)seedRef suriSuffix:(NSString*)suriSuffix message:(NSString*)message resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(substrateAddressWithRef:(int64_t)seedRef suriSuffix:(NSString*)suriSuffix prefix:(NSUInteger*)prefix resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(substrateSignWithRef:(int64_t)seedRef suri_suffix:(NSString*)suri_suffix message:(NSString*)message resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(substrateAddressWithRef:(int64_t)seedRef suri_suffix:(NSString*)suri_suffix prefix:(NSUInteger*)prefix resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(brainWalletAddressWithRef:(int64_t)seedRef resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
@end
This diff is collapsed.
......@@ -29,6 +29,7 @@ import {
ScreenStack
} from './screens';
import { SeedRefStore } from 'stores/SeedRefStore';
import colors from 'styles/colors';
import '../ReactotronConfig';
import { AppProps, getLaunchArgs } from 'e2e/injections';
......@@ -51,6 +52,7 @@ export default function App(props: AppProps): React.ReactElement {
const [policyConfirmed, setPolicyConfirmed] = React.useState<boolean>(false);
const [dataLoaded, setDataLoaded] = React.useState<boolean>(false);
React.useEffect(() => {
const loadPolicyConfirmationAndMigrateData = async (): Promise<void> => {
const tocPP = await loadToCAndPPConfirmation();
......@@ -93,14 +95,16 @@ export default function App(props: AppProps): React.ReactElement {
return (
<SafeAreaProvider>
<UnstatedProvider>
<MenuProvider backHandler={true}>
<StatusBar barStyle="light-content" backgroundColor={colors.bg} />
<GlobalStateContext.Provider value={globalContext}>
<NavigationContainer>{renderStacks()}</NavigationContainer>
</GlobalStateContext.Provider>
</MenuProvider>
</UnstatedProvider>
<SeedRefStore>
<UnstatedProvider>
<MenuProvider backHandler={true}>
<StatusBar barStyle="light-content" backgroundColor={colors.bg} />
<GlobalStateContext.Provider value={globalContext}>
<NavigationContainer>{renderStacks()}</NavigationContainer>
</GlobalStateContext.Provider>
</MenuProvider>
</UnstatedProvider>
</SeedRefStore>
</SafeAreaProvider>
);
}
......
......@@ -31,10 +31,10 @@ import fontStyles from 'styles/fontStyles';
import { withAccountStore } from 'utils/HOC';
import { getIdentityName } from 'utils/identitiesUtils';
import {
getSeedPhrase,
navigateToLegacyAccountList,
resetNavigationTo,
resetNavigationWithNetworkChooser,
unlockSeedPhrase
resetNavigationWithNetworkChooser
} from 'utils/navigationHelpers';
import { Identity } from 'types/identityTypes';
......@@ -71,10 +71,10 @@ function IdentitiesSwitch({
): Promise<void> => {
await accounts.selectIdentity(identity);
setVisible(false);
if (screenName === 'AccountNetworkChooser') {
if (screenName === 'Main') {
resetNavigationTo(navigation, screenName, params);
} else if (screenName === 'IdentityBackup') {
const seedPhrase = await unlockSeedPhrase(navigation);
const seedPhrase = await getSeedPhrase(navigation);
resetNavigationWithNetworkChooser(navigation, screenName, {
isNew: false,
seedPhrase
......@@ -132,10 +132,7 @@ function IdentitiesSwitch({
<ButtonIcon
title={currentIdentityTitle}
onPress={(): Promise<void> =>
onIdentitySelectedAndNavigate(
currentIdentity,
'AccountNetworkChooser'
)
onIdentitySelectedAndNavigate(currentIdentity, 'Main')
}
iconType="antdesign"
iconName="user"
......@@ -198,7 +195,7 @@ function IdentitiesSwitch({
}
title={title}
onPress={(): Promise<void> =>
onIdentitySelectedAndNavigate(identity, 'AccountNetworkChooser')
onIdentitySelectedAndNavigate(identity, 'Main')
}
key={identity.encryptedSeed}
iconType="antdesign"
......
......@@ -14,49 +14,40 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
/**
* This is the current app's main landing page
*/
import React, { useState } from 'react';
import { ScrollView } from 'react-native';
import React, { FunctionComponent, useState } from 'react';
import { ScrollView, StyleSheet, Text, View } from 'react-native';
import {
SafeAreaScrollViewContainer,
SafeAreaViewContainer
} from 'components/SafeAreaContainer';
import { NetworkCard } from 'components/AccountCard';
import { SafeAreaViewContainer } from 'components/SafeAreaContainer';
import ScreenHeading, { IdentityHeading } from 'components/ScreenHeading';
import {
NETWORK_LIST,
UnknownNetworkKeys,
NetworkProtocols,
SubstrateNetworkKeys,
NetworkProtocols
UnknownNetworkKeys
} from 'constants/networkSpecs';
import testIDs from 'e2e/testIDs';
import colors from 'styles/colors';
import Button from 'components/Button';
import { NavigationAccountProps } from 'types/props';
import {
navigateToPathsList,
unlockSeedPhrase,
navigateToPathDetails
} from 'utils/navigationHelpers';
import { withAccountStore } from 'utils/HOC';
isEthereumNetworkParams,
isSubstrateNetworkParams,
isUnknownNetworkParams,
NetworkParams,
SubstrateNetworkParams
} from 'types/networkSpecsTypes';
import { NavigationAccountProps } from 'types/props';
import { alertPathDerivationError } from 'utils/alertUtils';
import {
getExistedNetworkKeys,
getIdentityName,
getPathsWithSubstrateNetworkKey
} from 'utils/identitiesUtils';
import ScreenHeading, { IdentityHeading } from 'components/ScreenHeading';
import fontStyles from 'styles/fontStyles';
import { NetworkCard } from 'components/AccountCard';
import {
NetworkParams,
SubstrateNetworkParams,
isEthereumNetworkParams,
isSubstrateNetworkParams,
isUnknownNetworkParams
} from 'types/networkSpecsTypes';
navigateToPathDetails,
navigateToPathsList,
unlockSeedPhrase
} from 'utils/navigationHelpers';
import { useSeedRef } from 'utils/seedRefHooks';
const excludedNetworks = [
UnknownNetworkKeys.UNKNOWN,
......@@ -67,65 +58,15 @@ if (!__DEV__) {
excludedNetworks.push(SubstrateNetworkKeys.KUSAMA_DEV);
}
function AccountNetworkChooser({
export default function NetworkSelector({
accounts,
navigation,
route
}: NavigationAccountProps<'AccountNetworkChooser'>): React.ReactElement {
}: NavigationAccountProps<'Main'>): React.ReactElement {
const isNew = route.params?.isNew ?? false;
const [shouldShowMoreNetworks, setShouldShowMoreNetworks] = useState(false);
const { identities, currentIdentity, loaded } = accounts.state;
const hasLegacyAccount = accounts.getAccounts().size !== 0;
const TextButton: FunctionComponent<{ text: string; isRecover: boolean }> = ({
text,
isRecover
}) => (
<Text
style={[fontStyles.quote, { textDecorationLine: 'underline' }]}
testID={
isRecover
? testIDs.AccountNetworkChooser.recoverButton
: testIDs.AccountNetworkChooser.createButton
}
onPress={(): void => navigation.navigate('IdentityNew', { isRecover })}
>
{text}
</Text>
);
const showOnboardingMessage = (): React.ReactElement => (
<SafeAreaScrollViewContainer
testID={testIDs.AccountNetworkChooser.noAccountScreen}
contentContainerStyle={styles.scrollContent}
>
<View style={styles.onboardingWrapper}>
<TextButton text="Create" isRecover={false} />
<Text style={fontStyles.quote}> or </Text>
<TextButton text="recover" isRecover={true} />
<Text style={fontStyles.quote}>your identity to get started.</Text>
{hasLegacyAccount && (
<Button
title="Show Legacy Accounts"
onPress={(): void => navigation.navigate('LegacyAccountList')}
small={true}
onlyText={true}
style={{ marginLeft: 0 }}
/>
)}
</View>
</SafeAreaScrollViewContainer>
);
const showNoCurrentIdentityMessage = (): React.ReactElement => (
<SafeAreaScrollViewContainer contentContainerStyle={styles.scrollContent}>
<View style={styles.onboardingWrapper}>
<Text style={fontStyles.quote}>
Select one of your identity to get started.
</Text>
</View>
</SafeAreaScrollViewContainer>
);
const { identities, currentIdentity } = accounts.state;
const seedRefHooks = useSeedRef(currentIdentity!.encryptedSeed);
const sortNetworkKeys = (
[, params1]: [any, NetworkParams],
......@@ -156,12 +97,12 @@ function AccountNetworkChooser({
networkParams: SubstrateNetworkParams
): Promise<void> => {
const { pathId } = networkParams;
const seedPhrase = await unlockSeedPhrase(navigation);
await unlockSeedPhrase(navigation, seedRefHooks.isSeedRefValid);
const fullPath = `//${pathId}`;
try {
await accounts.deriveNewPath(
fullPath,
seedPhrase,
seedRefHooks.substrateAddress,
networkKey,
`${networkParams.title} root`,
''
......@@ -173,9 +114,12 @@ function AccountNetworkChooser({
};
const deriveEthereumAccount = async (networkKey: string): Promise<void> => {
const seedPhrase = await unlockSeedPhrase(navigation);
await unlockSeedPhrase(navigation, seedRefHooks.isSeedRefValid);
try {
await accounts.deriveEthereumAccount(seedPhrase, networkKey);
await accounts.deriveEthereumAccount(
seedRefHooks.brainWalletAddress,
networkKey
);
navigateToPathsList(navigation, networkKey);
} catch (e) {
alertPathDerivationError(e.message);
......@@ -188,7 +132,7 @@ function AccountNetworkChooser({
onPress={(): void =>
navigation.navigate('PathDerivation', { parentPath: '' })
}
testID={testIDs.AccountNetworkChooser.addCustomNetworkButton}
testID={testIDs.Main.addCustomNetworkButton}
title="Create Custom Path"
networkColor={colors.bg}
/>
......@@ -201,7 +145,7 @@ function AccountNetworkChooser({
<NetworkCard
isAdd={true}
onPress={(): void => setShouldShowMoreNetworks(true)}
testID={testIDs.AccountNetworkChooser.addNewNetworkButton}
testID={testIDs.Main.addNewNetworkButton}
title="Add Network Account"
networkColor={colors.bg}
/>
......@@ -262,10 +206,6 @@ function AccountNetworkChooser({
}
};
if (!loaded) return <SafeAreaViewContainer />;
if (identities.length === 0) return showOnboardingMessage();
if (!currentIdentity) return showNoCurrentIdentityMessage();
const availableNetworks = getExistedNetworkKeys(currentIdentity!);
const networkList = Object.entries(NETWORK_LIST).filter(filterNetworkKeys);
networkList.sort(sortNetworkKeys);
......@@ -273,10 +213,7 @@ function AccountNetworkChooser({
return (
<SafeAreaViewContainer>
{renderScreenHeading()}
<ScrollView
bounces={false}
testID={testIDs.AccountNetworkChooser.chooserScreen}
>
<ScrollView bounces={false} testID={testIDs.Main.chooserScreen}>
{networkList.map(([networkKey, networkParams]) => {
const networkIndexSuffix = isEthereumNetworkParams(networkParams)
? networkParams.ethereumChainId
......@@ -284,9 +221,7 @@ function AccountNetworkChooser({
return (
<NetworkCard
key={networkKey}
testID={
testIDs.AccountNetworkChooser.networkButton + networkIndexSuffix
}
testID={testIDs.Main.networkButton + networkIndexSuffix}
networkKey={networkKey}
onPress={(): Promise<void> =>
onNetworkChosen(networkKey, networkParams)
......@@ -300,19 +235,3 @@ function AccountNetworkChooser({
</SafeAreaViewContainer>
);
}
export default withAccountStore(AccountNetworkChooser);
const styles = StyleSheet.create({
onboardingWrapper: {
alignItems: 'center',
flexDirection: 'row',
flexWrap: 'wrap'
},
scrollContent: {
flex: 1,
justifyContent: 'center',
padding: 16,
paddingBottom: 100
}
});
// 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/>.
import React from 'react';
import { Text, View } from 'react-native';
import { SafeAreaScrollViewContainer } from 'components/SafeAreaContainer';
import styles from 'modules/main/styles';
import fontStyles from 'styles/fontStyles';
export default function NoCurrentIdentity(): React.ReactElement {
return (
<SafeAreaScrollViewContainer contentContainerStyle={styles.scrollContent}>
<View style={styles.onboardingWrapper}>
<Text style={fontStyles.quote}>
Select one of your identity to get started.
</Text>
</View>
</SafeAreaScrollViewContainer>
);
}
// 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/>.
import { useNavigation } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import React from 'react';
import { Text, View } from 'react-native';
import Button from 'components/Button';
import { SafeAreaScrollViewContainer } from 'components/SafeAreaContainer';
import testIDs from 'e2e/testIDs';
import styles from 'modules/main/styles';
import fontStyles from 'styles/fontStyles';
import { RootStackParamList } from 'types/routes';
export default function OnBoardingView({
hasLegacyAccount
}: {
hasLegacyAccount: boolean;
}): React.ReactElement {
const navigation: StackNavigationProp<RootStackParamList> = useNavigation();
function TextButton({
text,
isRecover
}: {
text: string;
isRecover: boolean;
}): React.ReactElement {
return (
<Text
style={[fontStyles.quote, { textDecorationLine: 'underline' }]}
testID={
isRecover ? testIDs.Main.recoverButton : testIDs.Main.createButton
}
onPress={(): void => navigation.navigate('IdentityNew', { isRecover })}
>
{text}
</Text>
);
}
return (
<SafeAreaScrollViewContainer
testID={testIDs.Main.noAccountScreen}
contentContainerStyle={styles.scrollContent}
>
<View style={styles.onboardingWrapper}>
<TextButton text="Create" isRecover={false} />
<Text style={fontStyles.quote}> or </Text>
<TextButton text="recover" isRecover={true} />
<Text style={fontStyles.quote}>your identity to get started.</Text>
{hasLegacyAccount && (
<Button
title="Show Legacy Accounts"
onPress={(): void => navigation.navigate('LegacyAccountList')}
small={true}
onlyText={true}
style={{ marginLeft: 0 }}
/>
)}
</View>
</SafeAreaScrollViewContainer>
);
}
// 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/>.
import React from 'react';
import NoCurrentIdentity from 'modules/main/components/NoCurrentIdentity';
import { SafeAreaViewContainer } from 'components/SafeAreaContainer';
import OnBoardingView from 'modules/main/components/OnBoading';
import NetworkSelector from 'modules/main/components/NetworkSelector';
import { NavigationAccountProps } from 'types/props';
import { withAccountStore } from 'utils/HOC';
function Main(props: NavigationAccountProps<'Main'>): React.ReactElement {
const { identities, currentIdentity, loaded } = props.accounts.state;
const hasLegacyAccount = props.accounts.getAccounts().size !== 0;
if (!loaded) return <SafeAreaViewContainer />;
if (identities.length === 0)
return <OnBoardingView hasLegacyAccount={hasLegacyAccount} />;
if (!currentIdentity) return <NoCurrentIdentity />;
return <NetworkSelector {...props} />;
}
export default withAccountStore(Main);
import { StyleSheet } from 'react-native';
const styles = StyleSheet.create({
onboardingWrapper: {
alignItems: 'center',
flexDirection: 'row',
flexWrap: 'wrap'
},
scrollContent: {
flex: 1,
justifyContent: 'center',
padding: 16,
paddingBottom: 100
}
});
export default styles;
......@@ -24,8 +24,9 @@ import testIDs from 'e2e/testIDs';
import ScreenHeading from 'components/ScreenHeading';
import ButtonMainAction from 'components/ButtonMainAction';
import { NavigationAccountProps } from 'types/props';
import { withAccountStore } from 'utils/HOC';
import { unlockIdentitySeed } from 'utils/identitiesUtils';
import { withAccountStore, withTargetIdentity } from 'utils/HOC';
import { unlockIdentitySeedWithReturn } from 'utils/identitiesUtils';
import { useSeedRef } from 'utils/seedRefHooks';
function PinUnlock({
accounts,
......@@ -33,16 +34,27 @@ function PinUnlock({
}: NavigationAccountProps<'PinUnlock'>): React.ReactElement {
const [state, updateState, resetState] = usePinState();
const targetIdentity =
route.params.identity ?? accounts.state.currentIdentity;
route.params.identity ?? accounts.state.currentIdentity!;
const { createSeedRef } = useSeedRef(targetIdentity.encryptedSeed);
async function submit(): Promise<void> {
const { pin } = state;
if (pin.length >= 6 && targetIdentity) {
try {
const resolve = route.params.resolve;
const seedPhrase = await unlockIdentitySeed(pin, targetIdentity);
resetState();
resolve(seedPhrase);
if (route.params.shouldReturnSeed) {
const seedPhrase = await unlockIdentitySeedWithReturn(
pin,
targetIdentity,
createSeedRef
);
resetState();
resolve(seedPhrase);
} else {
await createSeedRef(pin);
resetState();
resolve();
}
} catch (e) {
updateState({ pin: '', pinMismatch: true });
//TODO record error times;
......@@ -77,4 +89,4 @@ function PinUnlock({
);
}
export default withAccountStore(PinUnlock);
export default withAccountStore(withTargetIdentity(PinUnlock));
......@@ -24,9 +24,8 @@ import testIDs from 'e2e/testIDs';
import ScreenHeading from 'components/ScreenHeading';
import ButtonMainAction from 'components/ButtonMainAction';
import { NavigationAccountProps } from 'types/props';
import { withAccountStore } from 'utils/HOC';
import { unlockIdentitySeed } from 'utils/identitiesUtils';
import { constructSURI } from 'utils/suri';
import { withAccountStore, withTargetIdentity } from 'utils/HOC';
import { useSeedRef } from 'utils/seedRefHooks';
function PinUnlockWithPassword({
accounts,
......@@ -35,28 +34,28 @@ function PinUnlockWithPassword({
const [state, updateState, resetState] = usePinState();
const [focusPassword, setFocusPassword] = useState<boolean>(false);
const targetIdentity =
route.params.identity ?? accounts.state.currentIdentity;
route.params.identity ?? accounts.state.currentIdentity!;
const { createSeedRef } = useSeedRef(targetIdentity.encryptedSeed);
async function submit(): Promise<void> {
const { pin, password } = state;