Implement Edit network form (#107)

* Add edit network form

* Set selected network when networks are updated

* Disable buttons and add spinner after submitting

* Display previous values in Edit network form

* Use error msgs form constants file

* Reset default networks on reset
This commit is contained in:
IshaVenikar 2024-04-19 16:25:53 +05:30 committed by Nabarun Gogoi
parent 8c0751f84b
commit 6d80f64a10
10 changed files with 213 additions and 25 deletions

View File

@ -28,6 +28,7 @@ import { EIP155_SIGNING_METHODS } from './utils/wallet-connect/EIP155Data';
import { getSignParamsMessage } from './utils/wallet-connect/helpers'; import { getSignParamsMessage } from './utils/wallet-connect/helpers';
import ApproveTransaction from './screens/ApproveTransaction'; import ApproveTransaction from './screens/ApproveTransaction';
import AddNetwork from './screens/AddNetwork'; import AddNetwork from './screens/AddNetwork';
import EditNetwork from './screens/EditNetwork';
import { COSMOS, EIP155 } from './utils/constants'; import { COSMOS, EIP155 } from './utils/constants';
import { useNetworks } from './context/NetworksContext'; import { useNetworks } from './context/NetworksContext';
import { NETWORK_METHODS } from './utils/wallet-connect/common-data'; import { NETWORK_METHODS } from './utils/wallet-connect/common-data';
@ -281,6 +282,13 @@ const App = (): React.JSX.Element => {
title: 'Add Network', title: 'Add Network',
}} }}
/> />
<Stack.Screen
name="EditNetwork"
component={EditNetwork}
options={{
title: 'Edit Network',
}}
/>
</Stack.Navigator> </Stack.Navigator>
<PairingModal <PairingModal
visible={modalVisible} visible={modalVisible}

View File

@ -183,6 +183,21 @@ const Accounts = ({ currentIndex, updateIndex }: AccountsProps) => {
</TouchableOpacity> </TouchableOpacity>
</View> </View>
<View style={styles.signLink}>
<TouchableOpacity
onPress={() => {
navigation.navigate('EditNetwork', {
selectedNetwork: selectedNetwork!,
});
}}>
<Text
variant="titleSmall"
style={[styles.hyperlink, { color: theme.colors.primary }]}>
Edit Network
</Text>
</TouchableOpacity>
</View>
{!selectedNetwork!.isDefault && ( {!selectedNetwork!.isDefault && (
<View style={styles.signLink}> <View style={styles.signLink}>
<TouchableOpacity <TouchableOpacity

View File

@ -5,8 +5,6 @@ import { retrieveNetworksData, storeNetworkData } from '../utils/accounts';
import { DEFAULT_NETWORKS, EIP155 } from '../utils/constants'; import { DEFAULT_NETWORKS, EIP155 } from '../utils/constants';
const NetworksContext = createContext<{ const NetworksContext = createContext<{
currentIndex: number;
setCurrentIndex: (index: number) => void;
networksData: NetworksDataState[]; networksData: NetworksDataState[];
setNetworksData: React.Dispatch<React.SetStateAction<NetworksDataState[]>>; setNetworksData: React.Dispatch<React.SetStateAction<NetworksDataState[]>>;
networkType: string; networkType: string;
@ -16,8 +14,6 @@ const NetworksContext = createContext<{
React.SetStateAction<NetworksDataState | undefined> React.SetStateAction<NetworksDataState | undefined>
>; >;
}>({ }>({
currentIndex: 0,
setCurrentIndex: () => {},
networksData: [], networksData: [],
setNetworksData: () => {}, setNetworksData: () => {},
networkType: '', networkType: '',
@ -33,7 +29,6 @@ const useNetworks = () => {
const NetworksProvider = ({ children }: { children: any }) => { const NetworksProvider = ({ children }: { children: any }) => {
const [networksData, setNetworksData] = useState<NetworksDataState[]>([]); const [networksData, setNetworksData] = useState<NetworksDataState[]>([]);
const [currentIndex, setCurrentIndex] = useState<number>(0);
const [networkType, setNetworkType] = useState<string>(EIP155); const [networkType, setNetworkType] = useState<string>(EIP155);
const [selectedNetwork, setSelectedNetwork] = useState<NetworksDataState>(); const [selectedNetwork, setSelectedNetwork] = useState<NetworksDataState>();
@ -50,14 +45,22 @@ const NetworksProvider = ({ children }: { children: any }) => {
setSelectedNetwork(retrievedNewNetworks[0]); setSelectedNetwork(retrievedNewNetworks[0]);
}; };
if (networksData.length === 0) {
fetchData(); fetchData();
}, []); }
}, [networksData]);
useEffect(() => {
setSelectedNetwork(prevSelectedNetwork => {
return networksData.find(
networkData => networkData.networkId === prevSelectedNetwork?.networkId,
);
});
}, [networksData]);
return ( return (
<NetworksContext.Provider <NetworksContext.Provider
value={{ value={{
currentIndex,
setCurrentIndex,
networksData, networksData,
setNetworksData, setNetworksData,
networkType, networkType,

View File

@ -20,13 +20,16 @@ import { StackParamsList } from '../types';
import { SelectNetworkType } from '../components/SelectNetworkType'; import { SelectNetworkType } from '../components/SelectNetworkType';
import { storeNetworkData } from '../utils/accounts'; import { storeNetworkData } from '../utils/accounts';
import { useNetworks } from '../context/NetworksContext'; import { useNetworks } from '../context/NetworksContext';
import { COSMOS, EIP155, CHAINID_DEBOUNCE_DELAY } from '../utils/constants'; import {
COSMOS,
EIP155,
CHAINID_DEBOUNCE_DELAY,
EMPTY_FIELD_ERROR,
INVALID_URL_ERROR,
} from '../utils/constants';
import { getCosmosAccounts } from '../utils/accounts'; import { getCosmosAccounts } from '../utils/accounts';
import ETH_CHAINS from '../assets/ethereum-chains.json'; import ETH_CHAINS from '../assets/ethereum-chains.json';
const EMPTY_FIELD_ERROR = 'Field cannot be empty';
const INVALID_URL_ERROR = 'Invalid URL';
const ethNetworkDataSchema = z.object({ const ethNetworkDataSchema = z.object({
chainId: z.string().nonempty({ message: EMPTY_FIELD_ERROR }), chainId: z.string().nonempty({ message: EMPTY_FIELD_ERROR }),
networkName: z.string().nonempty({ message: EMPTY_FIELD_ERROR }), networkName: z.string().nonempty({ message: EMPTY_FIELD_ERROR }),
@ -59,6 +62,7 @@ const AddNetwork = () => {
const { setNetworksData } = useNetworks(); const { setNetworksData } = useNetworks();
const [namespace, setNamespace] = useState<string>(EIP155); const [namespace, setNamespace] = useState<string>(EIP155);
const [isLoading, setIsLoading] = useState(false);
const networksFormDataSchema = const networksFormDataSchema =
namespace === EIP155 ? ethNetworkDataSchema : cosmosNetworkDataSchema; namespace === EIP155 ? ethNetworkDataSchema : cosmosNetworkDataSchema;
@ -114,6 +118,7 @@ const AddNetwork = () => {
const submit = useCallback( const submit = useCallback(
async (data: z.infer<typeof networksFormDataSchema>) => { async (data: z.infer<typeof networksFormDataSchema>) => {
setIsLoading(true);
const newNetworkData = { const newNetworkData = {
...data, ...data,
namespace, namespace,
@ -176,6 +181,7 @@ const AddNetwork = () => {
), ),
]); ]);
setIsLoading(false);
navigation.navigate('Laconic'); navigation.navigate('Laconic');
}, },
[navigation, namespace, setNetworksData], [navigation, namespace, setNetworksData],
@ -358,8 +364,12 @@ const AddNetwork = () => {
/> />
</> </>
)} )}
<Button mode="contained" onPress={handleSubmit(submit)}> <Button
Submit mode="contained"
loading={isLoading}
disabled={isLoading}
onPress={handleSubmit(submit)}>
{isLoading ? 'Adding' : 'Submit'}
</Button> </Button>
</ScrollView> </ScrollView>
); );

148
src/screens/EditNetwork.tsx Normal file
View File

@ -0,0 +1,148 @@
import React, { useCallback, useState } from 'react';
import { ScrollView, View } from 'react-native';
import { useForm, Controller } from 'react-hook-form';
import { TextInput, Button, HelperText, Text } from 'react-native-paper';
import { setInternetCredentials } from 'react-native-keychain';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import {
NativeStackNavigationProp,
NativeStackScreenProps,
} from '@react-navigation/native-stack';
import { useNavigation } from '@react-navigation/native';
import { StackParamsList } from '../types';
import styles from '../styles/stylesheet';
import { retrieveNetworksData } from '../utils/accounts';
import { useNetworks } from '../context/NetworksContext';
import { EMPTY_FIELD_ERROR, INVALID_URL_ERROR } from '../utils/constants';
const networksFormDataSchema = z.object({
networkName: z.string().nonempty({ message: EMPTY_FIELD_ERROR }),
rpcUrl: z.string().url({ message: INVALID_URL_ERROR }),
blockExplorerUrl: z
.string()
.url({ message: INVALID_URL_ERROR })
.or(z.literal('')),
});
type EditNetworkProps = NativeStackScreenProps<StackParamsList, 'EditNetwork'>;
const EditNetwork = ({ route }: EditNetworkProps) => {
const [isLoading, setIsLoading] = useState(false);
const { setNetworksData } = useNetworks();
const navigation =
useNavigation<NativeStackNavigationProp<StackParamsList>>();
const {
control,
formState: { errors },
handleSubmit,
} = useForm<z.infer<typeof networksFormDataSchema>>({
mode: 'onChange',
resolver: zodResolver(networksFormDataSchema),
});
const networkData = route.params.selectedNetwork;
const submit = useCallback(
async (data: z.infer<typeof networksFormDataSchema>) => {
setIsLoading(true);
const retrievedNetworksData = await retrieveNetworksData();
const newNetworkData = { ...networkData, ...data };
const index = retrievedNetworksData.findIndex(
network => network.networkId === networkData.networkId,
);
retrievedNetworksData.splice(index, 1, newNetworkData);
await setInternetCredentials(
'networks',
'_',
JSON.stringify(retrievedNetworksData),
);
setNetworksData(retrievedNetworksData);
setIsLoading(false);
navigation.navigate('Laconic');
},
[networkData, navigation, setNetworksData],
);
return (
<ScrollView contentContainerStyle={styles.signPage}>
<View>
<Text style={styles.subHeading}>
Edit {networkData?.networkName} details
</Text>
</View>
<Controller
control={control}
defaultValue={networkData.networkName}
name="networkName"
render={({ field: { onChange, onBlur, value } }) => (
<>
<TextInput
mode="outlined"
label="Network Name"
value={value}
onBlur={onBlur}
onChangeText={textValue => onChange(textValue)}
/>
<HelperText type="error">{errors.networkName?.message}</HelperText>
</>
)}
/>
<Controller
control={control}
name="rpcUrl"
defaultValue={networkData.rpcUrl}
render={({ field: { onChange, onBlur, value } }) => (
<>
<TextInput
mode="outlined"
label="New RPC URL"
onBlur={onBlur}
value={value}
onChangeText={textValue => onChange(textValue)}
/>
<HelperText type="error">{errors.rpcUrl?.message}</HelperText>
</>
)}
/>
<Controller
control={control}
defaultValue={networkData.blockExplorerUrl}
name="blockExplorerUrl"
render={({ field: { onChange, onBlur, value } }) => (
<>
<TextInput
mode="outlined"
value={value}
label="Block Explorer URL (Optional)"
onBlur={onBlur}
onChangeText={textValue => onChange(textValue)}
/>
<HelperText type="error">
{errors.blockExplorerUrl?.message}
</HelperText>
</>
)}
/>
<Button
mode="contained"
loading={isLoading}
disabled={isLoading}
onPress={handleSubmit(submit)}>
{isLoading ? 'Adding' : 'Submit'}
</Button>
</ScrollView>
);
};
export default EditNetwork;

View File

@ -92,16 +92,13 @@ const HomeScreen = () => {
}; };
const confirmResetWallet = useCallback(async () => { const confirmResetWallet = useCallback(async () => {
const updatedNetworks = networksData.filter(
networkData => networkData.isDefault,
);
setNetworksData(updatedNetworks);
setSelectedNetwork(undefined);
setIsWalletCreated(false); setIsWalletCreated(false);
setIsWalletCreating(false); setIsWalletCreating(false);
setAccounts([]); setAccounts([]);
setCurrentIndex(0); setCurrentIndex(0);
await resetWallet(updatedNetworks); setNetworksData([]);
setSelectedNetwork(undefined);
await resetWallet();
const sessions = web3wallet!.getActiveSessions(); const sessions = web3wallet!.getActiveSessions();
Object.keys(sessions).forEach(async sessionId => { Object.keys(sessions).forEach(async sessionId => {
@ -114,7 +111,6 @@ const HomeScreen = () => {
hideResetDialog(); hideResetDialog();
}, [ }, [
networksData,
setAccounts, setAccounts,
setActiveSessions, setActiveSessions,
setCurrentIndex, setCurrentIndex,

View File

@ -204,7 +204,9 @@ const styles = StyleSheet.create({
}, },
subHeading: { subHeading: {
textAlign: 'center', textAlign: 'center',
fontWeight: '600', fontWeight: 'bold',
marginBottom: 10,
marginTop: 10,
}, },
centerText: { centerText: {
textAlign: 'center', textAlign: 'center',

View File

@ -26,6 +26,9 @@ export type StackParamsList = {
WalletConnect: undefined; WalletConnect: undefined;
AddSession: undefined; AddSession: undefined;
AddNetwork: undefined; AddNetwork: undefined;
EditNetwork: {
selectedNetwork: NetworksDataState;
};
}; };
export type Account = { export type Account = {

View File

@ -225,13 +225,13 @@ const retrieveSingleAccount = async (
return loadedAccounts.find(account => account.address === address); return loadedAccounts.find(account => account.address === address);
}; };
const resetWallet = async (networks: NetworksDataState[]) => { const resetWallet = async () => {
try { try {
await Promise.all([ await Promise.all([
resetInternetCredentials('mnemonicServer'), resetInternetCredentials('mnemonicServer'),
resetKeyServers(EIP155), resetKeyServers(EIP155),
resetKeyServers(COSMOS), resetKeyServers(COSMOS),
setInternetCredentials('networks', '_', JSON.stringify(networks)), setInternetCredentials('networks', '_', JSON.stringify([])),
]); ]);
} catch (error) { } catch (error) {
console.error('Error resetting wallet:', error); console.error('Error resetting wallet:', error);

View File

@ -26,3 +26,6 @@ export const DEFAULT_NETWORKS = [
]; ];
export const CHAINID_DEBOUNCE_DELAY = 250; export const CHAINID_DEBOUNCE_DELAY = 250;
export const EMPTY_FIELD_ERROR = 'Field cannot be empty';
export const INVALID_URL_ERROR = 'Invalid URL';