Merge pull request #1201 from nymtech/feature/multi-address-wallet
Feature/multi address wallet
This commit is contained in:
@@ -42,7 +42,9 @@
|
||||
"react-hook-form": "^7.14.2",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"semver": "^6.3.0",
|
||||
"string-to-color": "^2.2.2",
|
||||
"use-clipboard-copy": "^0.2.0",
|
||||
"uuid": "^8.3.2",
|
||||
"yup": "^0.32.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -71,6 +73,7 @@
|
||||
"@types/react-router": "^5.1.18",
|
||||
"@types/react-router-dom": "^5.1.8",
|
||||
"@types/semver": "^7.3.8",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.13.0",
|
||||
"@typescript-eslint/parser": "^5.13.0",
|
||||
"babel-loader": "^8.2.2",
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import { Accounts } from 'src/components/Accounts';
|
||||
import { v4 as uuid4 } from 'uuid';
|
||||
|
||||
export default {
|
||||
title: 'Wallet / Multi Account',
|
||||
component: Accounts,
|
||||
} as ComponentMeta<typeof Accounts>;
|
||||
|
||||
const Template: ComponentStory<typeof Accounts> = (args) => (
|
||||
<Box display="flex" alignContent="center">
|
||||
<Accounts {...args} />
|
||||
</Box>
|
||||
);
|
||||
|
||||
export const Default = Template.bind({});
|
||||
Default.args = {
|
||||
storedAccounts: [{ name: 'Account 1', address: uuid4() }],
|
||||
};
|
||||
|
||||
export const MultipleAccounts = Template.bind({});
|
||||
MultipleAccounts.args = {
|
||||
storedAccounts: [
|
||||
{ name: 'Account 1', address: uuid4() },
|
||||
{ name: 'Account 2', address: uuid4() },
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,295 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
IconButton,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
TextField,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import stc from 'string-to-color';
|
||||
import { Add, CircleTwoTone, Close, Edit } from '@mui/icons-material';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export type TAccount = {
|
||||
name: string;
|
||||
address: string;
|
||||
};
|
||||
|
||||
type TDialog = 'Accounts' | 'Add' | 'Edit';
|
||||
|
||||
const AccountColor = ({ address }: { address: string }) => <CircleTwoTone sx={{ color: stc(address) }} />;
|
||||
|
||||
const AccountItem = ({
|
||||
name,
|
||||
address,
|
||||
selected,
|
||||
onSelect,
|
||||
onEdit,
|
||||
}: {
|
||||
name: string;
|
||||
address: string;
|
||||
selected: boolean;
|
||||
onSelect: () => void;
|
||||
onEdit: () => void;
|
||||
}) => (
|
||||
<ListItem disablePadding disableGutters sx={selected ? { bgcolor: 'rgba(33, 208, 115, 0.1)' } : {}}>
|
||||
<ListItemButton disableRipple onClick={onSelect}>
|
||||
<ListItemAvatar sx={{ minWidth: 0, mr: 2 }}>
|
||||
<AccountColor address={address} />
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={name} secondary={address} />
|
||||
<ListItemIcon>
|
||||
<IconButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onEdit();
|
||||
}}
|
||||
>
|
||||
<Edit />
|
||||
</IconButton>
|
||||
</ListItemIcon>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
);
|
||||
|
||||
const AccountsModal = ({
|
||||
show,
|
||||
accounts,
|
||||
selectedAccount,
|
||||
onClose,
|
||||
onAccountSelect,
|
||||
onAddAccount,
|
||||
onEditAccount,
|
||||
}: {
|
||||
show: boolean;
|
||||
accounts: TAccount[];
|
||||
selectedAccount: TAccount['address'];
|
||||
onClose: () => void;
|
||||
onAccountSelect: (account: TAccount) => void;
|
||||
onAddAccount: () => void;
|
||||
onEditAccount: (acc: TAccount) => void;
|
||||
}) => (
|
||||
<Dialog open={show} onClose={onClose} fullWidth hideBackdrop>
|
||||
<DialogTitle>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Typography variant="h6">Accounts</Typography>
|
||||
<IconButton onClick={onClose}>
|
||||
<Close />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Typography variant="body1" sx={{ color: 'grey.600' }}>
|
||||
Switch between accounts
|
||||
</Typography>
|
||||
</DialogTitle>
|
||||
<DialogContent sx={{ padding: 0 }}>
|
||||
{accounts.map(({ name, address }) => (
|
||||
<AccountItem
|
||||
name={name}
|
||||
address={address}
|
||||
onSelect={() => {
|
||||
onAccountSelect({ name, address });
|
||||
onClose();
|
||||
}}
|
||||
onEdit={() => onEditAccount({ name, address })}
|
||||
selected={selectedAccount === address}
|
||||
key={address}
|
||||
/>
|
||||
))}
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ p: 3 }}>
|
||||
<Button
|
||||
fullWidth
|
||||
disableElevation
|
||||
variant="contained"
|
||||
size="large"
|
||||
startIcon={<Add fontSize="small" />}
|
||||
onClick={onAddAccount}
|
||||
>
|
||||
Add new account
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
const AddAccountModal = ({
|
||||
show,
|
||||
onClose,
|
||||
onAdd,
|
||||
}: {
|
||||
show: boolean;
|
||||
onClose: () => void;
|
||||
onAdd: (accountName: string) => void;
|
||||
}) => {
|
||||
const [accountName, setAccountName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (!show) setAccountName('');
|
||||
}, [show]);
|
||||
|
||||
return (
|
||||
<Dialog open={show} onClose={onClose} fullWidth hideBackdrop>
|
||||
<DialogTitle>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Typography variant="h6">Add new account</Typography>
|
||||
<IconButton onClick={onClose}>
|
||||
<Close />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Typography variant="body1" sx={{ color: 'grey.600' }}>
|
||||
New wallet address
|
||||
</Typography>
|
||||
</DialogTitle>
|
||||
<DialogContent sx={{ p: 0 }}>
|
||||
<Box sx={{ px: 3, mt: 1 }}>
|
||||
<TextField
|
||||
label="Account name"
|
||||
fullWidth
|
||||
value={accountName}
|
||||
onChange={(e) => setAccountName(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ p: 3 }}>
|
||||
<Button
|
||||
fullWidth
|
||||
disableElevation
|
||||
variant="contained"
|
||||
size="large"
|
||||
startIcon={<Add fontSize="small" />}
|
||||
onClick={() => onAdd(accountName)}
|
||||
disabled={!accountName.length}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const EditAccountModal = ({
|
||||
account,
|
||||
show,
|
||||
onClose,
|
||||
onEdit,
|
||||
}: {
|
||||
account?: TAccount;
|
||||
show: boolean;
|
||||
onClose: () => void;
|
||||
onEdit: (account: TAccount) => void;
|
||||
}) => {
|
||||
const [accountName, setAccountName] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
setAccountName(account ? account?.name : '');
|
||||
}, [account]);
|
||||
|
||||
return (
|
||||
<Dialog open={show} onClose={onClose} fullWidth hideBackdrop>
|
||||
<DialogTitle>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Typography variant="h6">Edit account name</Typography>
|
||||
<IconButton onClick={onClose}>
|
||||
<Close />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Typography variant="body1" sx={{ color: 'grey.600' }}>
|
||||
New wallet address
|
||||
</Typography>
|
||||
</DialogTitle>
|
||||
<DialogContent sx={{ p: 0 }}>
|
||||
<Box sx={{ px: 3, mt: 1 }}>
|
||||
<TextField
|
||||
label="Account name"
|
||||
fullWidth
|
||||
value={accountName}
|
||||
onChange={(e) => setAccountName(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ p: 3 }}>
|
||||
<Button
|
||||
fullWidth
|
||||
disableElevation
|
||||
variant="contained"
|
||||
size="large"
|
||||
onClick={() => account && onEdit({ ...account, name: accountName })}
|
||||
disabled={!accountName?.length}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export const Accounts = ({ storedAccounts }: { storedAccounts: TAccount[] }) => {
|
||||
const [accounts, setAccounts] = useState(storedAccounts);
|
||||
const [selectedAccount, setSelectedAccount] = useState(accounts[0]);
|
||||
const [accountToEdit, setAccountToEdit] = useState<TAccount>();
|
||||
const [dialogToDisplasy, setDialogToDisplay] = useState<TDialog>();
|
||||
|
||||
useEffect(() => {
|
||||
const selected = accounts.find((acc) => acc.address === selectedAccount.address);
|
||||
if (selected) setSelectedAccount(selected);
|
||||
}, [accounts]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
startIcon={<AccountColor address={selectedAccount.address} />}
|
||||
color="inherit"
|
||||
onClick={() => setDialogToDisplay('Accounts')}
|
||||
size="large"
|
||||
disableRipple
|
||||
>
|
||||
{selectedAccount.name}
|
||||
</Button>
|
||||
<AccountsModal
|
||||
show={dialogToDisplasy === 'Accounts'}
|
||||
onClose={() => setDialogToDisplay(undefined)}
|
||||
accounts={accounts}
|
||||
onAccountSelect={(acc) => setSelectedAccount(acc)}
|
||||
selectedAccount={selectedAccount.address}
|
||||
onAddAccount={() => {
|
||||
setDialogToDisplay('Add');
|
||||
}}
|
||||
onEditAccount={(acc) => {
|
||||
setAccountToEdit(acc);
|
||||
setDialogToDisplay('Edit');
|
||||
}}
|
||||
/>
|
||||
<AddAccountModal
|
||||
show={dialogToDisplasy === 'Add'}
|
||||
onClose={() => {
|
||||
setDialogToDisplay(undefined);
|
||||
}}
|
||||
onAdd={(name) => {
|
||||
setAccounts((accs) => [...accs, { address: uuidv4(), name }]);
|
||||
setDialogToDisplay('Accounts');
|
||||
}}
|
||||
/>
|
||||
<EditAccountModal
|
||||
show={dialogToDisplasy === 'Edit'}
|
||||
account={accountToEdit}
|
||||
onClose={() => {
|
||||
setDialogToDisplay('Accounts');
|
||||
}}
|
||||
onEdit={(account) => {
|
||||
setAccounts((accs) => accs.map((acc) => (acc.address === account.address ? account : acc)));
|
||||
setDialogToDisplay('Accounts');
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from 'react';
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import { Box, Typography } from '@mui/material';
|
||||
import { Box } from '@mui/material';
|
||||
import { ClientAddressDisplay } from './ClientAddress';
|
||||
|
||||
export default {
|
||||
|
||||
@@ -14,10 +14,11 @@ export const NymCard: React.FC<{
|
||||
title: string | React.ReactElement;
|
||||
subheader?: string;
|
||||
Action?: React.ReactNode;
|
||||
Icon?: any;
|
||||
Icon?: React.ReactNode;
|
||||
noPadding?: boolean;
|
||||
}> = ({ title, subheader, Action, Icon, noPadding, children }) => (
|
||||
<Card variant="outlined" sx={{ overflow: 'auto' }}>
|
||||
borderless?: boolean;
|
||||
}> = ({ title, subheader, Action, Icon, noPadding, borderless, children }) => (
|
||||
<Card variant="outlined" sx={{ overflow: 'auto', ...(borderless && { border: 'none', dropShadow: 'none' }) }}>
|
||||
<CardHeader
|
||||
sx={{ p: 3, color: 'nym.background.dark' }}
|
||||
title={<Title title={title} Icon={Icon} />}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import { Box, Typography } from '@mui/material';
|
||||
|
||||
export const Title: React.FC<{ title: string | React.ReactNode; Icon: any }> = ({ title, Icon }) => (
|
||||
export const Title: React.FC<{ title: string | React.ReactNode; Icon?: React.ReactNode }> = ({ title, Icon }) => (
|
||||
<Box display="flex" alignItems="center">
|
||||
{Icon && <Icon sx={{ mr: 1 }} />}{' '}
|
||||
{Icon}
|
||||
<Typography variant="h6" sx={{ fontWeight: 600 }}>
|
||||
{title}
|
||||
</Typography>
|
||||
|
||||
@@ -137,7 +137,7 @@ export const VestingCard = () => {
|
||||
<NymCard
|
||||
title="Vesting Schedule"
|
||||
data-testid="check-unvested-tokens"
|
||||
Icon={InfoOutlined}
|
||||
Icon={<InfoOutlined />}
|
||||
Action={
|
||||
<IconButton
|
||||
onClick={async () => {
|
||||
|
||||
@@ -5080,6 +5080,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d"
|
||||
integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==
|
||||
|
||||
"@types/uuid@^8.3.4":
|
||||
version "8.3.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc"
|
||||
integrity sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==
|
||||
|
||||
"@types/webpack-env@^1.16.0":
|
||||
version "1.16.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.16.3.tgz#b776327a73e561b71e7881d0cd6d34a1424db86a"
|
||||
@@ -7234,6 +7239,11 @@ colorette@^2.0.10, colorette@^2.0.14:
|
||||
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.16.tgz#713b9af84fdb000139f04546bd4a93f62a5085da"
|
||||
integrity sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==
|
||||
|
||||
colornames@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/colornames/-/colornames-1.1.1.tgz#f8889030685c7c4ff9e2a559f5077eb76a816f96"
|
||||
integrity sha1-+IiQMGhcfE/54qVZ9Qd+t2qBb5Y=
|
||||
|
||||
colors@1.4.0, colors@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/colors/-/colors-1.4.0.tgz#c50491479d4c1bdaed2c9ced32cf7c7dc2360f78"
|
||||
@@ -10258,6 +10268,11 @@ he@^1.2.0:
|
||||
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
|
||||
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
|
||||
|
||||
hex-rgb@^4.1.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/hex-rgb/-/hex-rgb-4.3.0.tgz#af5e974e83bb2fefe44d55182b004ec818c07776"
|
||||
integrity sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==
|
||||
|
||||
highlight.js@^10.1.1, highlight.js@~10.7.0:
|
||||
version "10.7.3"
|
||||
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531"
|
||||
@@ -12316,6 +12331,11 @@ lodash.merge@^4.6.2:
|
||||
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
|
||||
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
|
||||
|
||||
lodash.padend@^4.6.1:
|
||||
version "4.6.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.padend/-/lodash.padend-4.6.1.tgz#53ccba047d06e158d311f45da625f4e49e6f166e"
|
||||
integrity sha1-U8y6BH0G4VjTEfRdpiX05J5vFm4=
|
||||
|
||||
lodash.template@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab"
|
||||
@@ -12331,11 +12351,21 @@ lodash.templatesettings@^4.0.0:
|
||||
dependencies:
|
||||
lodash._reinterpolate "^3.0.0"
|
||||
|
||||
lodash.trimstart@^4.5.1:
|
||||
version "4.5.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.trimstart/-/lodash.trimstart-4.5.1.tgz#8ff4dec532d82486af59573c39445914e944a7f1"
|
||||
integrity sha1-j/TexTLYJIavWVc8OURZFOlEp/E=
|
||||
|
||||
lodash.uniq@4.5.0, lodash.uniq@^4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
||||
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
|
||||
|
||||
lodash.words@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.words/-/lodash.words-4.2.0.tgz#5ecfeaf8ecf8acaa8e0c8386295f1993c9cf4036"
|
||||
integrity sha1-Xs/q+Oz4rKqODIOGKV8Zk8nPQDY=
|
||||
|
||||
lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
@@ -15666,6 +15696,11 @@ reusify@^1.0.4:
|
||||
resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76"
|
||||
integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==
|
||||
|
||||
rgb-hex@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/rgb-hex/-/rgb-hex-3.0.0.tgz#eab0168cc1279563b18a14605315389142e2e487"
|
||||
integrity sha512-8h7ZcwxCBDKvchSWbWngJuSCqJGQ6nDuLLg+QcRyQDbX9jMWt+PpPeXAhSla0GOooEomk3lCprUpGkMdsLjKyg==
|
||||
|
||||
rifm@^0.12.1:
|
||||
version "0.12.1"
|
||||
resolved "https://registry.yarnpkg.com/rifm/-/rifm-0.12.1.tgz#8fa77f45b7f1cda2a0068787ac821f0593967ac4"
|
||||
@@ -16468,6 +16503,18 @@ string-length@^4.0.1:
|
||||
char-regex "^1.0.2"
|
||||
strip-ansi "^6.0.0"
|
||||
|
||||
string-to-color@^2.2.2:
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/string-to-color/-/string-to-color-2.2.2.tgz#46210bf7777dc9198dcdf997bd18ae6749cc9a73"
|
||||
integrity sha512-XeA2goP7PNsSlz8RRn6KhYswnMf5Tl+38ajfy8n4oZJyMGC4qqKgHNHsZ/3qwvr42NRIjf9eSr721SyetDeMkA==
|
||||
dependencies:
|
||||
colornames "^1.1.1"
|
||||
hex-rgb "^4.1.0"
|
||||
lodash.padend "^4.6.1"
|
||||
lodash.trimstart "^4.5.1"
|
||||
lodash.words "^4.2.0"
|
||||
rgb-hex "^3.0.0"
|
||||
|
||||
string-width@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
|
||||
|
||||
Reference in New Issue
Block a user