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

refactor: state management and extract unstated (#660)

* alert backbones

* extract global state into context

* create and use context for alert

* make lint happy

* add actions to alert

* refine alert styles

* replace all the alert with context

* refactor context

* install unstated next

* Create AccountsContext.ts

* remove withAccountStore

* refactor accountStore with Context

* use withCurrentIdentity HOC

* make linter happy

* fix props types

* use scanner context

* refactor registries store and scanner context

* refactor txStore

* remove unstated from account unlock

* remove state when scanning

* update alerts

* upgrade detox

* make lint happy

* fix identity manipulation e2e test

* refactor account store

* safe area view for container

* fix scanning

* fix signing issue with react hooks

* remove debugging code

* clean hoc and rename accountsStore

* update types

* fix types

* remove unused functions

* revert detox upgrade

* fix lint warning

* update ios version on e2e
parent d6968405
Pipeline #101973 failed with stages
in 2 minutes and 28 seconds
......@@ -45,9 +45,9 @@
"@polkadot/util": "2.11.1",
"@polkadot/util-crypto": "2.11.1",
"@react-native-community/masked-view": "^0.1.6",
"@react-navigation/native": "^5.6.1",
"@react-navigation/stack": "^5.6.2",
"@react-native-community/netinfo": "^5.9.3",
"@react-navigation/native": "^5.7.1",
"@react-navigation/stack": "^5.7.1",
"bignumber.js": "^9.0.0",
"hoist-non-react-statics": "^3.3.0",
"node-libs-react-native": "^1.0.3",
......@@ -68,7 +68,6 @@
"react-native-tabs": "^1.0.9",
"react-native-vector-icons": "^6.6.0",
"readable-stream": "^3.4.0",
"unstated": "^2.1.1",
"vm-browserify": "1.1.0"
},
"devDependencies": {
......@@ -128,7 +127,7 @@
"build": "yarn xcbuild:githubActions",
"type": "ios.simulator",
"device": {
"os": "iOS 13.5",
"os": "iOS 13.6",
"type": "iPhone 8"
}
},
......
......@@ -19,7 +19,6 @@ import 'utils/iconLoader';
import * as React from 'react';
import { StatusBar, StyleSheet, View, YellowBox } from 'react-native';
import { NavigationContainer } from '@react-navigation/native';
import { Provider as UnstatedProvider } from 'unstated';
import { MenuProvider } from 'react-native-popup-menu';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import NavigationBar from 'react-native-navbar-color';
......@@ -30,13 +29,19 @@ import {
ScreenStack
} from './screens';
import { SeedRefStore } from 'stores/SeedRefStore';
import { useScannerContext, ScannerContext } from 'stores/ScannerContext';
import { useAccountContext, AccountsContext } from 'stores/AccountsContext';
import CustomAlert from 'components/CustomAlert';
import { SeedRefsContext, useSeedRefStore } from 'stores/SeedRefStore';
import colors from 'styles/colors';
import '../ReactotronConfig';
import { AppProps, getLaunchArgs } from 'e2e/injections';
import { GlobalState, GlobalStateContext } from 'stores/globalStateContext';
import { loadToCAndPPConfirmation } from 'utils/db';
import { migrateAccounts, migrateIdentity } from 'utils/migrationUtils';
import {
GlobalState,
GlobalStateContext,
useGlobalStateContext
} from 'stores/globalStateContext';
import { AlertStateContext, useAlertContext } from 'stores/alertContext';
export default function App(props: AppProps): React.ReactElement {
getLaunchArgs(props);
......@@ -54,32 +59,15 @@ 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();
setPolicyConfirmed(tocPP);
if (!tocPP) {
await migrateAccounts();
await migrateIdentity();
}
};
setDataLoaded(true);
loadPolicyConfirmationAndMigrateData();
}, []);
const globalContext: GlobalState = {
dataLoaded,
policyConfirmed,
setDataLoaded,
setPolicyConfirmed
};
const alertContext = useAlertContext();
const globalContext: GlobalState = useGlobalStateContext();
const seedRefContext = useSeedRefStore();
const accountsContext = useAccountContext();
const scannerContext = useScannerContext();
const renderStacks = (): React.ReactElement => {
if (dataLoaded) {
return policyConfirmed ? (
if (globalContext.dataLoaded) {
return globalContext.policyConfirmed ? (
<AppNavigator />
) : (
<TocAndPrivacyPolicyNavigator />
......@@ -99,19 +87,24 @@ export default function App(props: AppProps): React.ReactElement {
return (
<SafeAreaProvider>
<SeedRefStore>
<UnstatedProvider>
<MenuProvider backHandler={true}>
<StatusBar
barStyle="light-content"
backgroundColor={colors.background.app}
/>
<GlobalStateContext.Provider value={globalContext}>
<NavigationContainer>{renderStacks()}</NavigationContainer>
</GlobalStateContext.Provider>
</MenuProvider>
</UnstatedProvider>
</SeedRefStore>
<AccountsContext.Provider value={accountsContext}>
<ScannerContext.Provider value={scannerContext}>
<GlobalStateContext.Provider value={globalContext}>
<AlertStateContext.Provider value={alertContext}>
<SeedRefsContext.Provider value={seedRefContext}>
<MenuProvider backHandler={true}>
<StatusBar
barStyle="light-content"
backgroundColor={colors.background.app}
/>
<CustomAlert />
<NavigationContainer>{renderStacks()}</NavigationContainer>
</MenuProvider>
</SeedRefsContext.Provider>
</AlertStateContext.Provider>
</GlobalStateContext.Provider>
</ScannerContext.Provider>
</AccountsContext.Provider>
</SafeAreaProvider>
);
}
......
......@@ -20,8 +20,8 @@ import React from 'react';
import AccountCard from './AccountCard';
import PathCard from './PathCard';
import { AccountsContextState } from 'stores/AccountsContext';
import { FoundAccount } from 'types/identityTypes';
import AccountsStore from 'stores/AccountsStore';
import { isLegacyFoundAccount } from 'utils/identitiesUtils';
const CompatibleCard = ({
......@@ -30,7 +30,7 @@ const CompatibleCard = ({
titlePrefix
}: {
account: FoundAccount;
accountsStore: AccountsStore;
accountsStore: AccountsContextState;
titlePrefix?: string;
}): React.ReactElement =>
isLegacyFoundAccount(account) || account.isLegacy === undefined ? (
......
// Copyright 2015-2020 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, { useContext, useEffect, useMemo, useState } from 'react';
import { StyleSheet, View, Animated, Text, Easing } from 'react-native';
import Button from 'components/Button';
import { Action, AlertStateContext } from 'stores/alertContext';
import colors from 'styles/colors';
import fonts from 'styles/fonts';
import fontStyles from 'styles/fontStyles';
export default function CustomAlert(): React.ReactElement {
const { title, alertIndex, message, actions } = useContext(AlertStateContext);
/* eslint-disable-next-line react-hooks/exhaustive-deps */
const animatedValue = useMemo(() => new Animated.Value(1), [alertIndex]);
const [alertDisplay, setAlertDisplay] = useState<boolean>(false);
useEffect(() => {
if (alertIndex === 0) return;
setAlertDisplay(true);
if (actions.length === 0) {
Animated.timing(animatedValue, {
duration: 1000,
easing: Easing.poly(8),
toValue: 0,
useNativeDriver: false
}).start(() => {
setAlertDisplay(false);
});
}
/* eslint-disable-next-line react-hooks/exhaustive-deps */
}, [alertIndex]);
const renderActions = (action: Action, index: number): React.ReactElement => (
<Button
onlyText={true}
small={true}
key={'alert' + index}
testID={action.testID}
title={action.text}
onPress={(): any => {
if (action.onPress) {
action.onPress();
}
setAlertDisplay(false);
}}
style={styles.button}
textStyles={
action.onPress ? styles.buttonBoldText : styles.buttonLightText
}
/>
);
if (alertDisplay) {
return (
<Animated.View style={{ ...styles.background, opacity: animatedValue }}>
<View style={styles.body}>
{title !== '' && <Text style={styles.textTitle}>{title}</Text>}
<Text style={styles.textMessage}>{message}</Text>
{actions !== [] && (
<View style={styles.actionsContainer}>
{actions.map(renderActions)}
</View>
)}
</View>
</Animated.View>
);
} else {
return <View />;
}
}
const styles = StyleSheet.create({
actionsContainer: {
flexDirection: 'row',
justifyContent: 'space-around',
marginTop: 20
},
background: {
alignItems: 'center',
justifyContent: 'center',
position: 'absolute',
top: 80,
width: '100%',
zIndex: 100
},
body: {
backgroundColor: colors.background.alert,
paddingHorizontal: 20,
paddingVertical: 20,
width: '90%'
},
button: {
marginVertical: 0
},
buttonBoldText: {
fontFamily: fonts.robotoMonoMedium
},
buttonLightText: {
fontFamily: fonts.robotoMono
},
textMessage: {
...fontStyles.h2
},
textTitle: {
paddingVertical: 10,
...fontStyles.h1
}
});
......@@ -72,9 +72,12 @@ export default class CustomScrollView extends React.PureComponent<
this.setState({ visibleHeight: height })
}
scrollEventThrottle={16}
onScroll={Animated.event([
{ nativeEvent: { contentOffset: { y: this.state.indicator } } }
])}
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: this.state.indicator } } }],
{
useNativeDriver: false
}
)}
{...this.props}
>
{this.props.children}
......
......@@ -15,7 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { StackNavigationProp } from '@react-navigation/stack';
import React, { useState } from 'react';
import React, { useContext, useState } from 'react';
import { ScrollView, StyleSheet, View } from 'react-native';
import { useNavigation } from '@react-navigation/native';
......@@ -23,12 +23,11 @@ import ButtonIcon from './ButtonIcon';
import Separator from './Separator';
import TransparentBackground from './TransparentBackground';
import { AccountsContext } from 'stores/AccountsContext';
import { RootStackParamList } from 'types/routes';
import AccountsStore from 'stores/AccountsStore';
import testIDs from 'e2e/testIDs';
import colors from 'styles/colors';
import fontStyles from 'styles/fontStyles';
import { withAccountStore } from 'utils/HOC';
import { getIdentityName } from 'utils/identitiesUtils';
import {
unlockAndReturnSeed,
......@@ -46,14 +45,11 @@ function ButtonWithArrow(props: {
return <ButtonIcon {...props} {...i_arrowOptions} />;
}
function IdentitiesSwitch({
accounts
}: {
accounts: AccountsStore;
}): React.ReactElement {
function IdentitiesSwitch({}: {}): React.ReactElement {
const accountsStore = useContext(AccountsContext);
const navigation: StackNavigationProp<RootStackParamList> = useNavigation();
const [visible, setVisible] = useState(false);
const { currentIdentity, identities } = accounts.state;
const { currentIdentity, identities, accounts } = accountsStore.state;
// useEffect(() => {
// const firstLogin: boolean = identities.length === 0;
// if (currentIdentity === null && !firstLogin) {
......@@ -77,7 +73,7 @@ function IdentitiesSwitch({
screenName: RouteName,
params?: RootStackParamList[RouteName]
): Promise<void> => {
await accounts.selectIdentity(identity);
await accountsStore.selectIdentity(identity);
setVisible(false);
if (screenName === 'Main') {
resetNavigationTo(navigation, screenName, params);
......@@ -95,7 +91,7 @@ function IdentitiesSwitch({
const onLegacyListClicked = (): void => {
setVisible(false);
navigateToLegacyAccountList(navigation);
accounts.resetCurrentIdentity();
accountsStore.resetCurrentIdentity();
};
const renderIdentityOptions = (identity: Identity): React.ReactElement => {
......@@ -240,7 +236,7 @@ function IdentitiesSwitch({
<View style={styles.card}>
{renderCurrentIdentityCard()}
{renderIdentities()}
{accounts.getAccounts().size > 0 && (
{accounts.size > 0 && (
<>
<ButtonIcon
title="Legacy Accounts"
......@@ -318,4 +314,4 @@ const i_arrowOptions = {
textStyle: { ...fontStyles.a_text, color: colors.signal.main }
};
export default withAccountStore(IdentitiesSwitch);
export default IdentitiesSwitch;
......@@ -16,7 +16,7 @@
import { useNavigation } from '@react-navigation/native';
import { StackNavigationProp } from '@react-navigation/stack';
import React from 'react';
import React, { useContext } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import testIDs from '../../test/e2e/testIDs';
......@@ -24,6 +24,7 @@ import testIDs from '../../test/e2e/testIDs';
import PathCard from './PathCard';
import Separator from './Separator';
import { AlertStateContext } from 'stores/alertContext';
import colors from 'styles/colors';
import TouchableItem from 'components/TouchableItem';
import fontStyles from 'styles/fontStyles';
......@@ -44,7 +45,7 @@ import { alertPathDerivationError } from 'utils/alertUtils';
import { RootStackParamList } from 'types/routes';
type Props = {
accounts: AccountsStoreStateWithIdentity;
accountsStore: AccountsStoreStateWithIdentity;
currentIdentity: Identity;
pathGroup: PathGroup;
networkParams: SubstrateNetworkParams | UnknownNetworkParams;
......@@ -54,9 +55,10 @@ export default function PathGroupCard({
currentIdentity,
pathGroup,
networkParams,
accounts
accountsStore
}: Props): React.ReactElement {
const navigation = useNavigation<StackNavigationProp<RootStackParamList>>();
const { setAlert } = useContext(AlertStateContext);
const paths = pathGroup.paths;
const { isSeedRefValid, substrateAddress } = useSeedRef(
currentIdentity.encryptedSeed
......@@ -84,7 +86,7 @@ export default function PathGroupCard({
const nextPath = _getFullPath(nextIndex, isHardDerivation);
const name = removeSlash(`${pathGroup.title}${nextIndex}`);
try {
await accounts.deriveNewPath(
await accountsStore.deriveNewPath(
nextPath,
substrateAddress,
(networkParams as SubstrateNetworkParams).genesisHash,
......@@ -92,15 +94,10 @@ export default function PathGroupCard({
''
);
} catch (error) {
alertPathDerivationError(error.message);
alertPathDerivationError(setAlert, error.message);
}
};
const _deletePath = async (): Promise<void> => {
const targetPath = paths[paths.length - 1];
await accounts.deletePath(targetPath);
};
const headerTitle = removeSlash(pathGroup.title);
const headerCode = `//${networkParams.pathId}${pathGroup.title}`;
return (
......
......@@ -14,7 +14,7 @@
// 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, { ReactElement, useMemo, useState } from 'react';
import React, { ReactElement, useContext, useMemo, useState } from 'react';
import { BackHandler, FlatList, FlatListProps } from 'react-native';
import { useFocusEffect } from '@react-navigation/native';
......@@ -27,6 +27,7 @@ import {
UnknownNetworkKeys
} from 'constants/networkSpecs';
import testIDs from 'e2e/testIDs';
import { AlertStateContext } from 'stores/alertContext';
import colors from 'styles/colors';
import {
isEthereumNetworkParams,
......@@ -36,6 +37,7 @@ import {
} from 'types/networkSpecsTypes';
import { NavigationAccountIdentityProps } from 'types/props';
import { alertPathDerivationError } from 'utils/alertUtils';
import { withCurrentIdentity } from 'utils/HOC';
import { getExistedNetworkKeys, getIdentityName } from 'utils/identitiesUtils';
import {
navigateToPathDetails,
......@@ -55,16 +57,18 @@ if (!__DEV__) {
excludedNetworks.push(SubstrateNetworkKeys.KUSAMA_DEV);
}
export default function NetworkSelector({
accounts,
function NetworkSelector({
accountsStore,
navigation,
route
}: NavigationAccountIdentityProps<'Main'>): React.ReactElement {
const isNew = route.params?.isNew ?? false;
const [shouldShowMoreNetworks, setShouldShowMoreNetworks] = useState(false);
const { identities, currentIdentity } = accounts.state;
const { identities, currentIdentity } = accountsStore.state;
const seedRefHooks = useSeedRef(currentIdentity.encryptedSeed);
const { unlockWithoutPassword } = useUnlockSeed(seedRefHooks.isSeedRefValid);
const { setAlert } = useContext(AlertStateContext);
// catch android back button and prevent exiting the app
useFocusEffect(
React.useCallback((): any => {
......@@ -122,7 +126,7 @@ export default function NetworkSelector({
await unlockSeedPhrase(navigation, seedRefHooks.isSeedRefValid);
const fullPath = `//${pathId}`;
try {
await accounts.deriveNewPath(
await accountsStore.deriveNewPath(
fullPath,
seedRefHooks.substrateAddress,
networkKey,
......@@ -131,20 +135,20 @@ export default function NetworkSelector({
);
navigateToPathDetails(navigation, networkKey, fullPath);
} catch (error) {
alertPathDerivationError(error.message);
alertPathDerivationError(setAlert, error.message);
}
};
const deriveEthereumAccount = async (networkKey: string): Promise<void> => {
await unlockSeedPhrase(navigation, seedRefHooks.isSeedRefValid);
try {
await accounts.deriveEthereumAccount(
await accountsStore.deriveEthereumAccount(
seedRefHooks.brainWalletAddress,
networkKey
);
navigateToPathsList(navigation, networkKey);
} catch (e) {
alertPathDerivationError(e.message);
alertPathDerivationError(setAlert, e.message);
}
};
......@@ -252,3 +256,5 @@ export default function NetworkSelector({
</SafeAreaViewContainer>
);
}
export default withCurrentIdentity(NetworkSelector);
......@@ -14,21 +14,21 @@
// 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 React, { useContext } 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 {
NavigationAccountIdentityProps,
NavigationAccountProps
} from 'types/props';
import { withAccountStore } from 'utils/HOC';
import { AccountsContext } from 'stores/AccountsContext';
import { NavigationAccountIdentityProps, NavigationProps } from 'types/props';
function Main(props: NavigationAccountProps<'Main'>): React.ReactElement {
const { identities, currentIdentity, loaded } = props.accounts.state;
const hasLegacyAccount = props.accounts.getAccounts().size !== 0;
export default function Main(
props: NavigationProps<'Main'>
): React.ReactElement {
const accountsStore = useContext(AccountsContext);
const { identities, currentIdentity, loaded, accounts } = accountsStore.state;
const hasLegacyAccount = accounts.size !== 0;
if (!loaded) return <SafeAreaViewContainer />;
if (identities.length === 0)
......@@ -38,5 +38,3 @@ function Main(props: NavigationAccountProps<'Main'>): React.ReactElement {
<NetworkSelector {...(props as NavigationAccountIdentityProps<'Main'>)} />
);
}
export default withAccountStore(Main);
......@@ -18,12 +18,13 @@ import { GenericExtrinsicPayload } from '@polkadot/types';
import Call from '@polkadot/types/generic/Call';
import { formatBalance } from '@polkadot/util';
import { decodeAddress, encodeAddress } from '@polkadot/util-crypto';
import React, { useEffect, useState } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import { StyleSheet, Text, View, ViewStyle } from 'react-native';
import { AnyU8a, IExtrinsicEra, IMethod } from '@polkadot/types/types';
import { ExtrinsicEra } from '@polkadot/types/interfaces';
import RegistriesStore from 'stores/RegistriesStore';
import { AlertStateContext } from 'stores/alertContext';
import { RegistriesStoreState } from 'stores/RegistriesContext';
import colors from 'styles/colors';
import { SUBSTRATE_NETWORK_LIST } from 'constants/networkSpecs';
import { withRegistriesStore } from 'utils/HOC';
......@@ -38,7 +39,7 @@ type ExtrinsicPartProps = {
fallback?: string;
label: string;
networkKey: string;
registriesStore: RegistriesStore;
registriesStore: RegistriesStoreState;
value: AnyU8a | IMethod | IExtrinsicEra;
};
......@@ -55,6 +56,7 @@ const ExtrinsicPart = withRegistriesStore<ExtrinsicPartProps>(
const [formattedCallArgs, setFormattedCallArgs] = useState<any>();
const [tip, setTip] = useState<string>();
const [useFallback, setUseFallBack] = useState(false);
const { setAlert } = useContext(AlertStateContext);
const prefix = SUBSTRATE_NETWORK_LIST[networkKey].prefix;
useEffect(() => {
......@@ -113,7 +115,7 @@ const ExtrinsicPart = withRegistriesStore<ExtrinsicPartProps>(
formatArgs(call, methodArgs, 0);
setFormattedCallArgs(methodArgs);
} catch (e) {
alertDecodeError();
alertDecodeError(setAlert);
setUseFallBack(true);
}
}
......@@ -128,7 +130,7 @@ const ExtrinsicPart = withRegistriesStore<ExtrinsicPartProps>(
if (label === 'Tip' && !fallback) {
setTip(formatBalance(value as any));
}
}, [fallback, label, prefix, value, networkKey, registriesStore]);
}, [fallback, label, prefix, value, networkKey, registriesStore, setAlert]);