Skip to content

Commit

Permalink
chore: persist account visibility in UI
Browse files Browse the repository at this point in the history
  • Loading branch information
stanleyyconsensys committed Feb 24, 2025
1 parent 653bfe9 commit b733399
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 89 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import React, { useMemo, useState } from 'react';
import React, { useState } from 'react';
import Toastr from 'toastr2';

import { useMultiLanguage, useStarkNetSnap } from 'services';
import { useCurrentNetwork, useCurrentAccount, useAppSelector } from 'hooks';
import {
useCurrentNetwork,
useCurrentAccount,
useAccountVisibility,
MinAccountToHideError,
SwitchAccountError,
} from 'hooks';
import { Account } from 'types';
import { Button } from 'components/ui/atom/Button';
import { Scrollable } from 'components/ui/atom/Scrollable';
Expand All @@ -24,27 +30,13 @@ export const AccountListModalView = ({
onAddAccountClick: () => void;
}) => {
const toastr = new Toastr();
const { switchAccount, hideAccount, unHideAccount } = useStarkNetSnap();
const { switchAccount } = useStarkNetSnap();
const { translate } = useMultiLanguage();
const currentNework = useCurrentNetwork();
const { address: currentAddress } = useCurrentAccount();
const accounts = useAppSelector((state) => state.wallet.accounts);
const [visibility, setVisibility] = useState(true);
// Use useMemo to avoid re-rendering the component when the state changes
const [visibleAccounts, hiddenAccounts] = useMemo(() => {
const visibleAccounts: Account[] = [];
const hiddenAccounts: Account[] = [];
for (const account of accounts) {
// account.visibility = `undefined` refer to the case when previous account state doesnt include this field
// hence we consider it is `visible`
if (account.visibility === undefined || account.visibility === true) {
visibleAccounts.push(account);
} else {
hiddenAccounts.push(account);
}
}
return [visibleAccounts, hiddenAccounts];
}, [accounts]);
const { visibleAccounts, hiddenAccounts, showAccount, hideAccount } =
useAccountVisibility();
const chainId = currentNework?.chainId;

const preventDefaultMouseEvent = (event: React.MouseEvent) => {
Expand All @@ -56,7 +48,6 @@ export const AccountListModalView = ({

const onAccountSwitchClick = async (account: Account) => {
onClose();

await switchAccount(chainId, account.address);
};

Expand All @@ -65,15 +56,17 @@ export const AccountListModalView = ({
account: Account,
) => {
preventDefaultMouseEvent(event);

if (visibleAccounts.length < 2) {
toastr.error(translate('youCannotHideLastAccount'));
} else {
await hideAccount({
chainId,
address: account.address,
currentAddress,
});
try {
await hideAccount(account);
} catch (error) {
// TODO: Add translation
if (error instanceof MinAccountToHideError) {
toastr.error(translate('youCannotHideLastAccount'));
} else if (error instanceof SwitchAccountError) {
toastr.error(translate('switchAccountError'));
} else {
toastr.error(translate('hideAccountFail'));
}
}
};

Expand All @@ -82,11 +75,7 @@ export const AccountListModalView = ({
account: Account,
) => {
preventDefaultMouseEvent(event);

await unHideAccount({
chainId,
address: account.address,
});
await showAccount(account);
};

return (
Expand Down
1 change: 1 addition & 0 deletions packages/wallet-ui/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './redux';
export * from './useCurrentNetwork';
export * from './useCurrentAccount';
export * from './useScrollTo';
export * from './useAccountVisibility';
98 changes: 98 additions & 0 deletions packages/wallet-ui/src/hooks/useAccountVisibility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { useMemo } from 'react';

import { useStarkNetSnap } from 'services';
import { Account } from 'types';
import { setAccountVisibility } from 'slices/walletSlice';
import { useAppDispatch, useAppSelector } from './redux';
import { useCurrentNetwork } from './useCurrentNetwork';
import { useCurrentAccount } from './useCurrentAccount';

export class MinAccountToHideError extends Error {}

export class SwitchAccountError extends Error {}

/**
* A hook to manage account visibility
*
* @returns {visibleAccounts, hiddenAccounts, showAccount, hideAccount}
*/
export const useAccountVisibility = () => {
const dispatch = useAppDispatch();
const { switchAccount } = useStarkNetSnap();
const currentNework = useCurrentNetwork();
const currentAccount = useCurrentAccount();
const visibilities = useAppSelector(
(state) => state.wallet.visibility[currentNework.chainId],
);
const accounts = useAppSelector((state) => state.wallet.accounts);
const chainId = currentNework?.chainId;

// Use useMemo to avoid re-rendering the component when the state changes
const [visibleAccounts, hiddenAccounts] = useMemo(() => {
const visibleAccounts: Account[] = [];
const hiddenAccounts: Account[] = [];
for (const account of accounts) {
// account.visibility = `undefined` refer to the case when previous account state doesnt include this field
// hence we consider it is `visible`
if (
!Object.prototype.hasOwnProperty.call(
visibilities,
account.addressIndex,
) ||
visibilities[account.addressIndex] === true
) {
visibleAccounts.push(account);
} else {
hiddenAccounts.push(account);
}
}
return [visibleAccounts, hiddenAccounts];
}, [accounts, visibilities, chainId]);

const hideAccount = async (account: Account) => {
if (visibleAccounts.length < 2) {
throw new MinAccountToHideError();
}

toggleAccountVisibility(account, false);

if (account.addressIndex === currentAccount.addressIndex) {
// Find the next account by using the next ascending addressIndex.
const nextAccount = visibleAccounts
.sort((a, b) => a.addressIndex - b.addressIndex)
.find(
(account) => account.addressIndex !== currentAccount.addressIndex,
);

if (nextAccount) {
if ((await switchAccount(chainId, account.address)) === undefined) {
// `switchAccount` will not return an account if it fails / error
// Therefore, when account switch fails, we need to rollback the state
toggleAccountVisibility(account, true);
throw new SwitchAccountError();
}
}
}
};

const showAccount = async (account: Account) => {
toggleAccountVisibility(account, true);
};

const toggleAccountVisibility = (account: Account, visibility: boolean) => {
dispatch(
setAccountVisibility({
chainId,
index: account.addressIndex,
visibility: visibility,
}),
);
};

return {
visibleAccounts,
hiddenAccounts,
showAccount,
hideAccount,
};
};
54 changes: 0 additions & 54 deletions packages/wallet-ui/src/services/useStarkNetSnap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,58 +214,6 @@ export const useStarkNetSnap = () => {
dispatch(disableLoading());
};

const hideAccount = async ({
chainId,
address,
currentAddress,
}: {
chainId: string;
address: string;
currentAddress: string;
}) => {
try {
if (!loader.isLoading) {
dispatch(
enableLoadingWithMessage(`Hiding account ${shortenAddress(address)}`),
);
}
const account = await toggleAccountVisibility(chainId, address, false);
dispatch(updateAccount({ address, updates: { visibility: false } }));
if (account.address !== currentAddress) {
await initWalletData({
account,
chainId,
});
}
} catch (error) {
const toastr = new Toastr();
toastr.error('Failed to hide the account');
} finally {
dispatch(disableLoading());
}
};

const unHideAccount = async ({
chainId,
address,
}: {
chainId: string;
address: string;
}) => {
if (!loader.isLoading) {
dispatch(enableLoadingWithMessage(`Loading...`));
}
try {
await toggleAccountVisibility(chainId, address, true);
dispatch(updateAccount({ address, updates: { visibility: true } }));
} catch (err) {
const toastr = new Toastr();
toastr.error('Failed to show the account');
} finally {
dispatch(disableLoading());
}
};

const setAccount = async (chainId: string, currentAccount: Account) => {
const { upgradeRequired, deployRequired } = currentAccount;

Expand Down Expand Up @@ -948,8 +896,6 @@ export const useStarkNetSnap = () => {
loadLocale,
getNetworks,
getAccounts,
hideAccount,
unHideAccount,
switchAccount,
getCurrentAccount,
addNewAccount,
Expand Down
13 changes: 13 additions & 0 deletions packages/wallet-ui/src/slices/walletSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Transaction } from 'types';
import { ethers } from 'ethers';
import { defaultAccount } from 'utils/constants';
import defaultLocale from '../assets/locales/en.json';
import { constants } from 'starknet';

export interface WalletState {
connected: boolean;
Expand All @@ -19,6 +20,9 @@ export interface WalletState {
transactions: Transaction[];
transactionDeploy?: Transaction;
provider?: any; //TODO: metamask SDK is not export types
visibility: {
[key: string]: Record<string, boolean>;
};
}

const initialState: WalletState = {
Expand All @@ -34,12 +38,20 @@ const initialState: WalletState = {
transactions: [],
transactionDeploy: undefined,
provider: undefined,
visibility: {
[constants.StarknetChainId.SN_MAIN]: {},
[constants.StarknetChainId.SN_SEPOLIA]: {},
},
};

export const walletSlice = createSlice({
name: 'wallet',
initialState,
reducers: {
setAccountVisibility: (state, { payload }) => {
const { chainId, index, visibility } = payload;
state.visibility[chainId][index] = visibility;
},
setLocale: (state, { payload }) => {
state.locale = payload;
},
Expand Down Expand Up @@ -155,6 +167,7 @@ export const walletSlice = createSlice({
});

export const {
setAccountVisibility,
setWalletConnection,
setForceReconnect,
setCurrentAccount,
Expand Down
2 changes: 1 addition & 1 deletion packages/wallet-ui/src/store/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const persistConfig = {
const walletPersistConfig = {
key: 'wallet',
storage,
whitelist: ['forceReconnect'],
whitelist: ['forceReconnect', 'visibility'],
};

const networkPersistConfig = {
Expand Down

0 comments on commit b733399

Please sign in to comment.