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 { createNativeStackNavigator } from '@react-navigation/native-stack';
import SignMessage from './components/SignMessage'; import SignMessage from './components/SignMessage';
import { HomeScreen } from './components/HomeScreen'; import HomeScreen from './components/HomeScreen';
import { StackParamsList } from './types'; import { StackParamsList } from './types';

View File

@ -1,12 +1,13 @@
import { View } from 'react-native';
import React, { useState } from 'react'; 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 { useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { AccountsProps, StackParamsList, Account } from '../types'; import { AccountsProps, StackParamsList, Account } from '../types';
import { addAccount } from '../utils'; import { addAccount } from '../utils';
import styles from '../styles/stylesheet';
const Accounts: React.FC<AccountsProps> = ({ const Accounts: React.FC<AccountsProps> = ({
network, network,
@ -20,24 +21,44 @@ const Accounts: React.FC<AccountsProps> = ({
const [expanded, setExpanded] = useState(false); const [expanded, setExpanded] = useState(false);
const [isAccountCreating, setIsAccountCreating] = useState(false); const [isAccountCreating, setIsAccountCreating] = useState(false);
const handlePress = () => setExpanded(!expanded); const handlePress = () => setExpanded(!expanded);
const addAccountHandler = async () => { const addAccountHandler = async () => {
setIsAccountCreating(true); setIsAccountCreating(true);
const newAccount = await addAccount(network); const newAccount = await addAccount(network);
setIsAccountCreating(false); setIsAccountCreating(false);
newAccount && updateAccounts(newAccount); setExpanded(false);
updateIndex(selectedAccounts[selectedAccounts.length - 1].id + 1);
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') { const renderAccountItems = () =>
selectedAccounts = accounts.ethAccounts; selectedAccounts.map(account => (
} <List.Item
if (network === 'cosmos') { key={account.id}
selectedAccounts = accounts.cosmosAccounts; title={`Account ${account.id + 1}`}
} onPress={() => {
updateIndex(account.id);
setExpanded(false);
}}
/>
));
const theme = useTheme();
return ( return (
<View> <View>
@ -45,51 +66,43 @@ const Accounts: React.FC<AccountsProps> = ({
title={`Account ${currentIndex + 1}`} title={`Account ${currentIndex + 1}`}
expanded={expanded} expanded={expanded}
onPress={handlePress}> onPress={handlePress}>
{selectedAccounts && {renderAccountItems()}
selectedAccounts.map(account => (
<List.Item
key={account.id}
title={`Account ${account.id + 1}`}
onPress={() => {
updateIndex(account.id);
setExpanded(false);
}}
/>
))}
</List.Accordion> </List.Accordion>
<View style={{ alignItems: 'center', marginTop: 24 }}>
<View style={styles.addAccountButton}>
<Button <Button
mode="contained" mode="contained"
onPress={addAccountHandler} onPress={addAccountHandler}
loading={isAccountCreating}> loading={isAccountCreating}>
{isAccountCreating ? 'Adding' : 'Add Account'}{' '} {isAccountCreating ? 'Adding' : 'Add Account'}
</Button> </Button>
</View> </View>
<View style={{ marginTop: 24 }}>
<View style={styles.accountContainer}>
<Text variant="bodyLarge"> <Text variant="bodyLarge">
<Text style={{ fontWeight: '700' }}>Address: </Text> <Text style={styles.highlight}>Address: </Text>
{selectedAccounts && {selectedAccounts[currentIndex]?.address}
selectedAccounts[currentIndex] &&
selectedAccounts[currentIndex].address}
</Text> </Text>
<Text variant="bodyLarge"> <Text variant="bodyLarge">
<Text style={{ fontWeight: '700' }}>Public Key: </Text> <Text style={styles.highlight}>Public Key: </Text>
{selectedAccounts && {selectedAccounts[currentIndex]?.pubKey}
selectedAccounts[currentIndex] &&
selectedAccounts[currentIndex].pubKey}
</Text> </Text>
</View> </View>
<View style={{ alignItems: 'center', marginTop: 24 }}>
<Button <View style={styles.signLink}>
mode="contained" <TouchableOpacity
onPress={() => { onPress={() => {
navigation.navigate('SignMessage', { navigation.navigate('SignMessage', {
selectedNetwork: network, selectedNetwork: network,
accountInfo: selectedAccounts[currentIndex], accountInfo: selectedAccounts[currentIndex],
}); });
}}> }}>
Sign Message <Text
</Button> variant="titleSmall"
style={[styles.hyperlink, { color: theme.colors.primary }]}>
Sign Message
</Text>
</TouchableOpacity>
</View> </View>
</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 React, { useState } from 'react';
import { Alert, ScrollView, View } from 'react-native'; import { Alert, View } from 'react-native';
import { Text, Button, Dialog, Portal } from 'react-native-paper'; import { Button } from 'react-native-paper';
import { createWallet, resetWallet } from '../utils'; import { createWallet, resetWallet } from '../utils';
import { DialogComponent } from './Dialog'; import { DialogComponent } from './Dialog';
import { NetworkDropdown } from './NetworkDropdown'; import { NetworkDropdown } from './NetworkDropdown';
import { Account, AccountsState } from '../types'; import { Account, AccountsState } from '../types';
import Accounts from './Accounts'; import Accounts from './Accounts';
import CreateWallet from './CreateWallet';
import ResetWalletDialog from './ResetWalletDialog';
import styles from '../styles/stylesheet';
const HomeScreen = () => { const HomeScreen = () => {
const [isWalletCreated, setIsWalletCreated] = useState<boolean>(false); const [isWalletCreated, setIsWalletCreated] = useState<boolean>(false);
@ -27,12 +30,12 @@ const HomeScreen = () => {
const createWalletHandler = async () => { const createWalletHandler = async () => {
setIsWalletCreating(true); setIsWalletCreating(true);
await new Promise(resolve => setTimeout(resolve, 2000)); await new Promise(resolve => setTimeout(resolve, 2000));
const { ethWalletInfo, cosmosWalletInfo } = await createWallet(); const { ethAccounts, cosmosAccounts } = await createWallet();
ethWalletInfo && ethAccounts &&
cosmosWalletInfo && cosmosAccounts &&
setAccounts({ setAccounts({
ethAccounts: [...accounts.ethAccounts, ethWalletInfo], ethAccounts: [...accounts.ethAccounts, ethAccounts],
cosmosAccounts: [...accounts.cosmosAccounts, cosmosWalletInfo], cosmosAccounts: [...accounts.cosmosAccounts, cosmosAccounts],
}); });
setWalletDialog(true); setWalletDialog(true);
setIsWalletCreated(true); setIsWalletCreated(true);
@ -79,64 +82,53 @@ const HomeScreen = () => {
} }
}; };
return ( return (
<ScrollView style={{ marginTop: 24, paddingHorizontal: 24 }}> <View style={styles.appContainer}>
<DialogComponent <DialogComponent
visible={walletDialog} visible={walletDialog}
hideDialog={hideWalletDialog} hideDialog={hideWalletDialog}
contentText="Wallet created" contentText="Wallet created"
/> />
<Portal> <ResetWalletDialog
<Dialog visible={resetWalletDialog} onDismiss={hideResetDialog}> visible={resetWalletDialog}
<Dialog.Title>Reset Wallet</Dialog.Title> hideDialog={hideResetDialog}
<Dialog.Content> onConfirm={confirmResetWallet}
<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>
{isWalletCreated ? ( {isWalletCreated ? (
<View> <>
<NetworkDropdown <NetworkDropdown
selectedNetwork={network} selectedNetwork={network}
updateNetwork={updateNetwork} updateNetwork={updateNetwork}
/> />
<Accounts <View style={styles.accountComponent}>
network={network} <Accounts
accounts={accounts} network={network}
currentIndex={currentIndex} accounts={accounts}
updateIndex={updateIndex} currentIndex={currentIndex}
updateAccounts={updateAccounts} updateIndex={updateIndex}
/> updateAccounts={updateAccounts}
<View style={{ marginTop: 200, alignSelf: 'center' }}> />
</View>
<View style={styles.resetContainer}>
<Button <Button
style={styles.resetButton}
mode="contained" mode="contained"
buttonColor="#B82B0D" buttonColor="#B82B0D"
onPress={async () => { onPress={() => {
setResetWalletDialog(true); setResetWalletDialog(true);
}}> }}>
Reset Wallet Reset Wallet
</Button> </Button>
</View> </View>
</View> </>
) : ( ) : (
<View> <CreateWallet
<View style={{ marginTop: 20, width: 150, alignSelf: 'center' }}> isWalletCreating={isWalletCreating}
<Button createWalletHandler={createWalletHandler}
mode="contained" />
loading={isWalletCreating}
onPress={createWalletHandler}>
{isWalletCreating ? 'Creating' : 'Create Wallet'}{' '}
</Button>
</View>
</View>
)} )}
</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 { List } from 'react-native-paper';
import { NetworkDropdownProps } from '../types'; import { NetworkDropdownProps } from '../types';
import styles from '../styles/stylesheet';
const NetworkDropdown: React.FC<NetworkDropdownProps> = ({ updateNetwork }) => { const NetworkDropdown: React.FC<NetworkDropdownProps> = ({ updateNetwork }) => {
const [expanded, setExpanded] = useState<boolean>(false); 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 ( return (
<View style={{ marginBottom: 20 }}> <View style={styles.networkDropdown}>
<List.Accordion <List.Accordion
title={title} title={selectedNetwork}
expanded={expanded} expanded={expanded}
onPress={expandNetworks}> onPress={() => setExpanded(!expanded)}>
<List.Item {networks.map(network => (
title="Ethereum" <List.Item
onPress={() => { key={network.value}
updateNetwork('eth'); title={network.displayName}
setTitle('Ethereum'); onPress={() =>
setExpanded(false); handleNetworkPress(network.value, network.displayName)
}} }
/> />
<List.Item ))}
title="Cosmos"
onPress={() => {
updateNetwork('cosmos');
setTitle('Cosmos');
setExpanded(false);
}}
/>
</List.Accordion> </List.Accordion>
</View> </View>
); );
}; };
const networks = [
{ value: 'eth', displayName: 'Ethereum' },
{ value: 'cosmos', displayName: 'Cosmos' },
];
export { NetworkDropdown }; 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 { StackParamsList } from '../types';
import { signMessage } from '../utils'; import { signMessage } from '../utils';
import styles from '../styles/stylesheet';
type SignProps = NativeStackScreenProps<StackParamsList, 'SignMessage'>; type SignProps = NativeStackScreenProps<StackParamsList, 'SignMessage'>;
@ -20,30 +21,43 @@ const SignMessage = ({ route }: SignProps) => {
if (!account) { if (!account) {
throw new Error('Account is not valid'); 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); Alert.alert('Signature', signedMessage);
} }
}; };
return ( return (
<ScrollView style={{ marginTop: 24, paddingHorizontal: 24 }}> <ScrollView style={styles.signPage}>
<View style={{ marginTop: 24, marginBottom: 30 }}> <View style={styles.accountInfo}>
<Text variant="bodyLarge"> <View>
<Text style={{ fontWeight: '700' }}>Address: </Text> <Text variant="headlineSmall">
{account && account.address} {account && `Account ${account.id + 1}`}
</Text> </Text>
<Text variant="bodyLarge"> </View>
<Text style={{ fontWeight: '700' }}>Public Key: </Text> <View style={styles.accountContainer}>
{account && account.pubKey} <Text variant="bodyLarge">
</Text> <Text style={styles.highlight}>Address: </Text>
{account?.address}
</Text>
<Text variant="bodyLarge">
<Text style={styles.highlight}>Public Key: </Text>
{account?.pubKey}
</Text>
</View>
</View> </View>
<TextInput <TextInput
mode="outlined" mode="outlined"
placeholder="Enter your message" placeholder="Enter your message"
onChangeText={text => setMessage(text)} onChangeText={text => setMessage(text)}
value={message} value={message}
/> />
<View style={{ marginTop: 20, width: 150, alignSelf: 'center' }}>
<View style={styles.signButton}>
<Button mode="contained" onPress={signMessageHandler}> <Button mode="contained" onPress={signMessageHandler}>
Sign Sign
</Button> </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 'text-encoding-polyfill';
import { AppRegistry } from 'react-native'; import { AppRegistry } from 'react-native';
import { PaperProvider } from 'react-native-paper'; import { PaperProvider } from 'react-native-paper';

View File

@ -1,32 +1,59 @@
import { StyleSheet } from 'react-native'; import { StyleSheet } from 'react-native';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
headerContainer: { createWalletContainer: {
padding: 15, marginTop: 20,
alignItems: 'center', width: 150,
justifyContent: 'center', alignSelf: 'center',
}, },
headerText: { signLink: {
color: 'black', alignItems: 'flex-end',
fontSize: 26, marginTop: 24,
fontWeight: 'bold',
}, },
sectionContainer: { hyperlink: {
marginTop: 32, fontWeight: '500',
paddingHorizontal: 24, textDecorationLine: 'underline',
},
sectionTitle: {
fontSize: 22,
fontWeight: '600',
},
sectionDescription: {
marginTop: 8,
fontSize: 18,
fontWeight: '400',
}, },
highlight: { highlight: {
fontWeight: '700', 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; export default styles;

View File

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