Refactor code (#18)

* Refactored accounts and sign message component

* Change sign message to hyperlink

* Refactor network dropdown

* Add types to utils

* Import react in index.js

* Use components for create wallet and reset dialog

* Remove inline styles from accounts component

* Remove inline styles from components

* Remove incorrectly placed async

* Make app responsive using flex

* Make review changes

---------

Co-authored-by: Adw8 <adwait@deepstacksoft.com>
This commit is contained in:
Adwait Gharpure 2024-02-19 11:39:42 +05:30 committed by Ashwin Phatak
parent 8685c94151
commit cad0b6fae5
13 changed files with 276 additions and 199 deletions

View File

@ -4,7 +4,7 @@ import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import SignMessage from './components/SignMessage';
import { HomeScreen } from './components/HomeScreen';
import HomeScreen from './components/HomeScreen';
import { StackParamsList } from './types';

View File

@ -1,12 +1,13 @@
import { View } from 'react-native';
import React, { useState } from 'react';
import { Button, List, Text } from 'react-native-paper';
import { TouchableOpacity, View } from 'react-native';
import { Button, List, Text, useTheme } from 'react-native-paper';
import { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { AccountsProps, StackParamsList, Account } from '../types';
import { addAccount } from '../utils';
import styles from '../styles/stylesheet';
const Accounts: React.FC<AccountsProps> = ({
network,
@ -20,24 +21,44 @@ const Accounts: React.FC<AccountsProps> = ({
const [expanded, setExpanded] = useState(false);
const [isAccountCreating, setIsAccountCreating] = useState(false);
const handlePress = () => setExpanded(!expanded);
const addAccountHandler = async () => {
setIsAccountCreating(true);
const newAccount = await addAccount(network);
setIsAccountCreating(false);
newAccount && updateAccounts(newAccount);
updateIndex(selectedAccounts[selectedAccounts.length - 1].id + 1);
setExpanded(false);
if (newAccount) {
updateAccounts(newAccount);
updateIndex(selectedAccounts[selectedAccounts.length - 1].id + 1);
}
};
let selectedAccounts: Account[] = [];
const selectedAccounts: Account[] = (() => {
switch (network) {
case 'eth':
return accounts.ethAccounts;
case 'cosmos':
return accounts.cosmosAccounts;
default:
return [];
}
})();
if (network === 'eth') {
selectedAccounts = accounts.ethAccounts;
}
if (network === 'cosmos') {
selectedAccounts = accounts.cosmosAccounts;
}
const renderAccountItems = () =>
selectedAccounts.map(account => (
<List.Item
key={account.id}
title={`Account ${account.id + 1}`}
onPress={() => {
updateIndex(account.id);
setExpanded(false);
}}
/>
));
const theme = useTheme();
return (
<View>
@ -45,51 +66,43 @@ const Accounts: React.FC<AccountsProps> = ({
title={`Account ${currentIndex + 1}`}
expanded={expanded}
onPress={handlePress}>
{selectedAccounts &&
selectedAccounts.map(account => (
<List.Item
key={account.id}
title={`Account ${account.id + 1}`}
onPress={() => {
updateIndex(account.id);
setExpanded(false);
}}
/>
))}
{renderAccountItems()}
</List.Accordion>
<View style={{ alignItems: 'center', marginTop: 24 }}>
<View style={styles.addAccountButton}>
<Button
mode="contained"
onPress={addAccountHandler}
loading={isAccountCreating}>
{isAccountCreating ? 'Adding' : 'Add Account'}{' '}
{isAccountCreating ? 'Adding' : 'Add Account'}
</Button>
</View>
<View style={{ marginTop: 24 }}>
<View style={styles.accountContainer}>
<Text variant="bodyLarge">
<Text style={{ fontWeight: '700' }}>Address: </Text>
{selectedAccounts &&
selectedAccounts[currentIndex] &&
selectedAccounts[currentIndex].address}
<Text style={styles.highlight}>Address: </Text>
{selectedAccounts[currentIndex]?.address}
</Text>
<Text variant="bodyLarge">
<Text style={{ fontWeight: '700' }}>Public Key: </Text>
{selectedAccounts &&
selectedAccounts[currentIndex] &&
selectedAccounts[currentIndex].pubKey}
<Text style={styles.highlight}>Public Key: </Text>
{selectedAccounts[currentIndex]?.pubKey}
</Text>
</View>
<View style={{ alignItems: 'center', marginTop: 24 }}>
<Button
mode="contained"
<View style={styles.signLink}>
<TouchableOpacity
onPress={() => {
navigation.navigate('SignMessage', {
selectedNetwork: network,
accountInfo: selectedAccounts[currentIndex],
});
}}>
Sign Message
</Button>
<Text
variant="titleSmall"
style={[styles.hyperlink, { color: theme.colors.primary }]}>
Sign Message
</Text>
</TouchableOpacity>
</View>
</View>
);

View File

@ -0,0 +1,26 @@
import { View } from 'react-native';
import React from 'react';
import { Button } from 'react-native-paper';
import { CreateWalletProps } from '../types';
import styles from '../styles/stylesheet';
const CreateWallet = ({
isWalletCreating,
createWalletHandler,
}: CreateWalletProps) => {
return (
<View>
<View style={styles.createWalletContainer}>
<Button
mode="contained"
loading={isWalletCreating}
onPress={createWalletHandler}>
{isWalletCreating ? 'Creating' : 'Create Wallet'}
</Button>
</View>
</View>
);
};
export default CreateWallet;

View File

@ -1,12 +1,15 @@
import React, { useState } from 'react';
import { Alert, ScrollView, View } from 'react-native';
import { Text, Button, Dialog, Portal } from 'react-native-paper';
import { Alert, View } from 'react-native';
import { Button } from 'react-native-paper';
import { createWallet, resetWallet } from '../utils';
import { DialogComponent } from './Dialog';
import { NetworkDropdown } from './NetworkDropdown';
import { Account, AccountsState } from '../types';
import Accounts from './Accounts';
import CreateWallet from './CreateWallet';
import ResetWalletDialog from './ResetWalletDialog';
import styles from '../styles/stylesheet';
const HomeScreen = () => {
const [isWalletCreated, setIsWalletCreated] = useState<boolean>(false);
@ -27,12 +30,12 @@ const HomeScreen = () => {
const createWalletHandler = async () => {
setIsWalletCreating(true);
await new Promise(resolve => setTimeout(resolve, 2000));
const { ethWalletInfo, cosmosWalletInfo } = await createWallet();
ethWalletInfo &&
cosmosWalletInfo &&
const { ethAccounts, cosmosAccounts } = await createWallet();
ethAccounts &&
cosmosAccounts &&
setAccounts({
ethAccounts: [...accounts.ethAccounts, ethWalletInfo],
cosmosAccounts: [...accounts.cosmosAccounts, cosmosWalletInfo],
ethAccounts: [...accounts.ethAccounts, ethAccounts],
cosmosAccounts: [...accounts.cosmosAccounts, cosmosAccounts],
});
setWalletDialog(true);
setIsWalletCreated(true);
@ -79,64 +82,53 @@ const HomeScreen = () => {
}
};
return (
<ScrollView style={{ marginTop: 24, paddingHorizontal: 24 }}>
<View style={styles.appContainer}>
<DialogComponent
visible={walletDialog}
hideDialog={hideWalletDialog}
contentText="Wallet created"
/>
<Portal>
<Dialog visible={resetWalletDialog} onDismiss={hideResetDialog}>
<Dialog.Title>Reset Wallet</Dialog.Title>
<Dialog.Content>
<Text variant="bodyMedium">Are you sure?</Text>
</Dialog.Content>
<Dialog.Actions>
<Button textColor="red" onPress={confirmResetWallet}>
Yes
</Button>
<Button onPress={hideResetDialog}>No</Button>
</Dialog.Actions>
</Dialog>
</Portal>
<ResetWalletDialog
visible={resetWalletDialog}
hideDialog={hideResetDialog}
onConfirm={confirmResetWallet}
/>
{isWalletCreated ? (
<View>
<>
<NetworkDropdown
selectedNetwork={network}
updateNetwork={updateNetwork}
/>
<Accounts
network={network}
accounts={accounts}
currentIndex={currentIndex}
updateIndex={updateIndex}
updateAccounts={updateAccounts}
/>
<View style={{ marginTop: 200, alignSelf: 'center' }}>
<View style={styles.accountComponent}>
<Accounts
network={network}
accounts={accounts}
currentIndex={currentIndex}
updateIndex={updateIndex}
updateAccounts={updateAccounts}
/>
</View>
<View style={styles.resetContainer}>
<Button
style={styles.resetButton}
mode="contained"
buttonColor="#B82B0D"
onPress={async () => {
onPress={() => {
setResetWalletDialog(true);
}}>
Reset Wallet
</Button>
</View>
</View>
</>
) : (
<View>
<View style={{ marginTop: 20, width: 150, alignSelf: 'center' }}>
<Button
mode="contained"
loading={isWalletCreating}
onPress={createWalletHandler}>
{isWalletCreating ? 'Creating' : 'Create Wallet'}{' '}
</Button>
</View>
</View>
<CreateWallet
isWalletCreating={isWalletCreating}
createWalletHandler={createWalletHandler}
/>
)}
</ScrollView>
</View>
);
};
export { HomeScreen };
export default HomeScreen;

View File

@ -3,38 +3,41 @@ import { View } from 'react-native';
import { List } from 'react-native-paper';
import { NetworkDropdownProps } from '../types';
import styles from '../styles/stylesheet';
const NetworkDropdown: React.FC<NetworkDropdownProps> = ({ updateNetwork }) => {
const [expanded, setExpanded] = useState<boolean>(false);
const [title, setTitle] = useState<string>('Ethereum');
const [selectedNetwork, setSelectedNetwork] = useState<string>('Ethereum');
const expandNetworks = () => setExpanded(!expanded);
const handleNetworkPress = (network: string, displayName: string) => {
updateNetwork(network);
setSelectedNetwork(displayName);
setExpanded(false);
};
return (
<View style={{ marginBottom: 20 }}>
<View style={styles.networkDropdown}>
<List.Accordion
title={title}
title={selectedNetwork}
expanded={expanded}
onPress={expandNetworks}>
<List.Item
title="Ethereum"
onPress={() => {
updateNetwork('eth');
setTitle('Ethereum');
setExpanded(false);
}}
/>
<List.Item
title="Cosmos"
onPress={() => {
updateNetwork('cosmos');
setTitle('Cosmos');
setExpanded(false);
}}
/>
onPress={() => setExpanded(!expanded)}>
{networks.map(network => (
<List.Item
key={network.value}
title={network.displayName}
onPress={() =>
handleNetworkPress(network.value, network.displayName)
}
/>
))}
</List.Accordion>
</View>
);
};
const networks = [
{ value: 'eth', displayName: 'Ethereum' },
{ value: 'cosmos', displayName: 'Cosmos' },
];
export { NetworkDropdown };

View File

@ -0,0 +1,28 @@
import React from 'react';
import { Portal, Dialog, Button, Text } from 'react-native-paper';
import { ResetDialogProps } from '../types';
const ResetWalletDialog = ({
visible,
hideDialog,
onConfirm,
}: ResetDialogProps) => {
return (
<Portal>
<Dialog visible={visible} onDismiss={hideDialog}>
<Dialog.Title>Reset Wallet</Dialog.Title>
<Dialog.Content>
<Text variant="bodyMedium">Are you sure?</Text>
</Dialog.Content>
<Dialog.Actions>
<Button textColor="red" onPress={onConfirm}>
Yes
</Button>
<Button onPress={hideDialog}>No</Button>
</Dialog.Actions>
</Dialog>
</Portal>
);
};
export default ResetWalletDialog;

View File

@ -1,29 +0,0 @@
import { PropsWithChildren } from 'react';
import { Text, View, useColorScheme } from 'react-native';
import { Colors } from 'react-native/Libraries/NewAppScreen';
import styles from '../styles/stylesheet';
type SectionProps = PropsWithChildren<{
title: string;
}>;
const Section = ({ title }: SectionProps): React.JSX.Element => {
const isDarkMode = useColorScheme() === 'dark';
return (
<View style={styles.sectionContainer}>
<Text
style={[
styles.sectionTitle,
{
color: isDarkMode ? Colors.white : Colors.black,
},
]}>
{title}
<Text style={styles.sectionDescription} />
</Text>
</View>
);
};
export { Section };

View File

@ -6,6 +6,7 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { StackParamsList } from '../types';
import { signMessage } from '../utils';
import styles from '../styles/stylesheet';
type SignProps = NativeStackScreenProps<StackParamsList, 'SignMessage'>;
@ -20,30 +21,43 @@ const SignMessage = ({ route }: SignProps) => {
if (!account) {
throw new Error('Account is not valid');
}
const signedMessage = await signMessage(message, network, account.id);
const signedMessage = await signMessage({
message,
network,
accountId: account.id,
});
Alert.alert('Signature', signedMessage);
}
};
return (
<ScrollView style={{ marginTop: 24, paddingHorizontal: 24 }}>
<View style={{ marginTop: 24, marginBottom: 30 }}>
<Text variant="bodyLarge">
<Text style={{ fontWeight: '700' }}>Address: </Text>
{account && account.address}
</Text>
<Text variant="bodyLarge">
<Text style={{ fontWeight: '700' }}>Public Key: </Text>
{account && account.pubKey}
</Text>
<ScrollView style={styles.signPage}>
<View style={styles.accountInfo}>
<View>
<Text variant="headlineSmall">
{account && `Account ${account.id + 1}`}
</Text>
</View>
<View style={styles.accountContainer}>
<Text variant="bodyLarge">
<Text style={styles.highlight}>Address: </Text>
{account?.address}
</Text>
<Text variant="bodyLarge">
<Text style={styles.highlight}>Public Key: </Text>
{account?.pubKey}
</Text>
</View>
</View>
<TextInput
mode="outlined"
placeholder="Enter your message"
onChangeText={text => setMessage(text)}
value={message}
/>
<View style={{ marginTop: 20, width: 150, alignSelf: 'center' }}>
<View style={styles.signButton}>
<Button mode="contained" onPress={signMessageHandler}>
Sign
</Button>

View File

@ -1,5 +0,0 @@
export const COSMOS_SIGNATURE =
'0x56da25d5a9704e0cd685d52ecee5c14bf6637fa2f95653e8499eac4e8285f37b2d9f446c027cac56f3b7840d1b3879ea943415190d7a358cdb3ee05451cdcf7c1c';
export const COSMOS_ADDRESS = 'cosmos1sulk9q5fmagur6m3pctmcnfeeku25gp2ectt75';
export const COSMOS_PUBKEY =
'cosmospub1addwnpepqt9d597c5f6zqqyxy3msrstyc7zl3vyvrl5ku02r4ueuwt5vusw4gmt70dd';

View File

@ -1,3 +1,4 @@
import React from 'react';
import 'text-encoding-polyfill';
import { AppRegistry } from 'react-native';
import { PaperProvider } from 'react-native-paper';

View File

@ -1,32 +1,59 @@
import { StyleSheet } from 'react-native';
const styles = StyleSheet.create({
headerContainer: {
padding: 15,
alignItems: 'center',
justifyContent: 'center',
createWalletContainer: {
marginTop: 20,
width: 150,
alignSelf: 'center',
},
headerText: {
color: 'black',
fontSize: 26,
fontWeight: 'bold',
signLink: {
alignItems: 'flex-end',
marginTop: 24,
},
sectionContainer: {
marginTop: 32,
paddingHorizontal: 24,
},
sectionTitle: {
fontSize: 22,
fontWeight: '600',
},
sectionDescription: {
marginTop: 8,
fontSize: 18,
fontWeight: '400',
hyperlink: {
fontWeight: '500',
textDecorationLine: 'underline',
},
highlight: {
fontWeight: '700',
},
accountContainer: {
marginTop: 24,
},
addAccountButton: {
marginTop: 24,
alignSelf: 'center',
},
accountComponent: {
flex: 4,
},
appContainer: {
flexGrow: 1,
marginTop: 24,
paddingHorizontal: 24,
},
resetContainer: {
flex: 1,
justifyContent: 'center',
},
resetButton: {
alignSelf: 'center',
},
signButton: {
marginTop: 20,
width: 150,
alignSelf: 'center',
},
signPage: {
paddingHorizontal: 24,
},
accountInfo: {
marginTop: 24,
marginBottom: 30,
},
networkDropdown: {
marginBottom: 20,
},
});
export default styles;

View File

@ -10,8 +10,8 @@ export type Account = {
};
export type WalletDetails = {
ethAccount: Account;
cosmosAccount: Account;
ethAccounts: Account | undefined;
cosmosAccounts: Account | undefined;
};
export type AccountsProps = {
@ -34,3 +34,20 @@ export type AccountsState = {
ethAccounts: Account[];
cosmosAccounts: Account[];
};
export type SignMessageParams = {
message: string;
network: string;
accountId: number;
};
export type CreateWalletProps = {
isWalletCreating: boolean;
createWalletHandler: () => Promise<void>;
};
export type ResetDialogProps = {
visible: boolean;
hideDialog: () => void;
onConfirm: () => void;
};

View File

@ -14,10 +14,9 @@ import {
import { AccountData, Secp256k1HdWallet } from '@cosmjs/amino';
import { stringToPath } from '@cosmjs/crypto';
const createWallet = async (): Promise<{
ethWalletInfo: { id: number; pubKey: string; address: string } | undefined;
cosmosWalletInfo: { id: number; pubKey: string; address: string } | undefined;
}> => {
import { Account, SignMessageParams, WalletDetails } from './types';
const createWallet = async (): Promise<WalletDetails> => {
try {
const mnemonic = utils.entropyToMnemonic(utils.randomBytes(32));
await setInternetCredentials('mnemonicServer', 'mnemonic', mnemonic);
@ -43,35 +42,26 @@ const createWallet = async (): Promise<{
await setInternetCredentials('eth:accountIndices', 'ethAccount', '0');
await setInternetCredentials('cosmos:accountIndices', 'cosmosAccount', '0');
const ethWalletInfo = {
const ethAccounts = {
id: 0,
pubKey: ethNode.publicKey,
address: ethAddress,
};
const cosmosWalletInfo = {
const cosmosAccounts = {
id: 0,
pubKey: cosmosNode.publicKey,
address: cosmosAddress,
};
return { ethWalletInfo, cosmosWalletInfo };
return { ethAccounts, cosmosAccounts };
} catch (error) {
console.error('Error creating HD wallet:', error);
return { ethWalletInfo: undefined, cosmosWalletInfo: undefined };
return { ethAccounts: undefined, cosmosAccounts: undefined };
}
};
const addAccount = async (
network: string,
): Promise<
| {
pubKey: string;
address: string;
id: number;
}
| undefined
> => {
const addAccount = async (network: string): Promise<Account | undefined> => {
try {
const mnemonicStore = await getInternetCredentials('mnemonicServer');
if (!mnemonicStore) {
@ -138,16 +128,16 @@ const addAccount = async (
}
};
const signMessage = async (
message: string,
walletType: string,
id: number,
): Promise<string | undefined> => {
switch (walletType) {
const signMessage = async ({
message,
network,
accountId,
}: SignMessageParams): Promise<string | undefined> => {
switch (network) {
case 'eth':
return await signEthMessage(message, id);
return await signEthMessage(message, accountId);
case 'cosmos':
return await signCosmosMessage(message, id);
return await signCosmosMessage(message, accountId);
default:
throw new Error('Invalid wallet type');
}