Add address copy button (#300)

* Add address copy button

* CR requests implemented

* Fix lint

Co-authored-by: VladStarostenko <38181661+VladStarostenko@users.noreply.github.com>
This commit is contained in:
Ivan Rukhavets 2020-04-02 12:30:02 +03:00 committed by GitHub
parent 1af44b4264
commit e3407782ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 162 additions and 25 deletions

View File

@ -22,6 +22,7 @@
"@types/bn.js": "^4.11.6",
"@types/enzyme": "^3.10.5",
"@types/enzyme-adapter-react-16": "^1.0.6",
"@types/react-copy-to-clipboard": "^4.3.0",
"@types/react-dom": "^16.9.5",
"@types/react-router": "^5.1.4",
"@types/react-router-dom": "^5.1.3",

View File

@ -2,8 +2,9 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React, { useState } from 'react';
import React, { useCallback, useState } from 'react';
import { Button, ButtonArea, Checkbox, MnemonicSeed, VerticalSpace, Warning } from '../../components';
import useToast from '../../hooks/useToast';
interface Props {
onNextStep: () => void;
@ -23,13 +24,18 @@ const onCopy = (): void => {
function Mnemonic ({ onNextStep, seed }: Props): React.ReactElement<Props> {
const [isMnemonicSaved, setIsMnemonicSaved] = useState(false);
const { show } = useToast();
const _onCopy = useCallback((): void => {
onCopy();
show('Copied');
}, [show]);
return (
<>
<Warning>Please write down your wallet’s mnemonic seed and keep it in a safe place. <br />
Mnemonic seed is used to restore your wallet. Keep it carefully in case you lose your assets.</Warning>
<MnemonicSeed
onCopy={onCopy}
onCopy={_onCopy}
seed={seed}
/>
<VerticalSpace />

View File

@ -11,6 +11,7 @@ import { setSS58Format } from '@polkadot/util-crypto';
import { Loading } from '../components';
import { AccountContext, ActionContext, AuthorizeReqContext, MediaContext, MetadataReqContext, SigningReqContext } from '../components/contexts';
import ToastProvider from '../components/Toast/ToastProvider';
import { subscribeAccounts, subscribeAuthorizeRequests, subscribeMetadataRequests, subscribeSigningRequests } from '../messaging';
import Accounts from './Accounts';
import Authorize from './Authorize';
@ -100,20 +101,22 @@ export default function Popup (): React.ReactElement<{}> {
<MediaContext.Provider value={cameraOn && mediaAllowed}>
<MetadataReqContext.Provider value={metaRequests}>
<SigningReqContext.Provider value={signRequests}>
<Switch>
<Route path='/account/create'><CreateAccount /></Route>
<Route path='/account/forget/:address'><Forget /></Route>
<Route path='/account/export/:address'><Export /></Route>
<Route path='/account/import-qr'><ImportQr /></Route>
<Route path='/account/import-seed'><ImportSeed /></Route>
<Route path='/account/derive/:address'><Derive /></Route>
<Route
exact
path='/'
>
<Root />
</Route>
</Switch>
<ToastProvider>
<Switch>
<Route path='/account/create'><CreateAccount /></Route>
<Route path='/account/forget/:address'><Forget /></Route>
<Route path='/account/export/:address'><Export /></Route>
<Route path='/account/import-qr'><ImportQr /></Route>
<Route path='/account/import-seed'><ImportSeed /></Route>
<Route path='/account/derive/:address'><Derive /></Route>
<Route
exact
path='/'
>
<Root />
</Route>
</Switch>
</ToastProvider>
</SigningReqContext.Provider>
</MetadataReqContext.Provider>
</MediaContext.Provider>

View File

@ -5,7 +5,8 @@
import { AccountJson } from '@polkadot/extension-base/background/types';
import { Chain } from '@polkadot/extension-chains/types';
import React, { useContext, useEffect, useRef, useState } from 'react';
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
import CopyToClipboard from 'react-copy-to-clipboard';
import styled from 'styled-components';
import settings from '@polkadot/ui-settings';
import { decodeAddress, encodeAddress } from '@polkadot/util-crypto';
@ -13,11 +14,13 @@ import { decodeAddress, encodeAddress } from '@polkadot/util-crypto';
import { AccountContext } from './contexts';
import Identicon from './Identicon';
import Svg from './Svg';
import Menu from './Menu';
import Menu from './Menu';
import CopyImg from '../assets/copy.svg';
import DetailsImg from '../assets/details.svg';
import useOutsideClick from '../hooks/useOutsideClick';
import useMetadata from '../hooks/useMetadata';
import useToast from '../hooks/useToast';
interface Props {
address?: string | null;
@ -69,6 +72,7 @@ function Address ({ actions, address, children, className, genesisHash, name }:
const [showActionsMenu, setShowActionsMenu] = useState(false);
const [moveMenuUp, setIsMovedMenu] = useState(false);
const actionsRef = useRef<HTMLDivElement>(null);
const { show } = useToast();
useOutsideClick(actionsRef, () => (showActionsMenu && setShowActionsMenu(!showActionsMenu)));
@ -91,7 +95,8 @@ function Address ({ actions, address, children, className, genesisHash, name }:
}, [showActionsMenu]);
const theme = ((chain && chain.icon) || 'polkadot') as 'polkadot';
const _onClick = (): void => setShowActionsMenu(!showActionsMenu);
const _onClick = useCallback((): void => setShowActionsMenu(!showActionsMenu), [showActionsMenu]);
const _onCopy = useCallback((): void => show('Copied'), [show]);
return (
<div className={className}>
@ -104,10 +109,18 @@ function Address ({ actions, address, children, className, genesisHash, name }:
/>
<Info>
<Name>{name || (account && account.name) || '<unknown>'}</Name>
<FullAddress>{formatted || '<unknown>'}</FullAddress>
{chain?.genesisHash && (
<Banner>{chain.name}</Banner>
)}
<CopyAddress>
<FullAddress>{formatted || '<unknown>'}</FullAddress>
{chain?.genesisHash && (
<Banner>{chain.name}</Banner>
)}
<CopyToClipboard text={(formatted && formatted) || ''} >
<Svg
onClick={_onCopy}
src={CopyImg}
/>
</CopyToClipboard>
</CopyAddress>
</Info>
{actions && (
<>
@ -134,6 +147,22 @@ function Address ({ actions, address, children, className, genesisHash, name }:
);
}
const CopyAddress = styled.div`
display: flex;
justify-content: space-between;
& ${Svg} {
width: 14px;
height: 14px;
margin-right: 10px;
background: ${({ theme }): string => theme.accountDotsIconColor};
&:hover {
background: ${({ theme }): string => theme.labelColor};
cursor: pointer;
}
}
`;
const AccountInfoRow = styled.div`
display: flex;
flex-direction: row;
@ -158,7 +187,7 @@ const Name = styled.div`
`;
const FullAddress = styled.div`
width: 300px;
width: 270px;
overflow: hidden;
text-overflow: ellipsis;
color: ${({ theme }): string => theme.labelColor};

View File

@ -0,0 +1,35 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React from 'react';
import styled from 'styled-components';
interface Props {
content: React.ReactChild;
className?: string;
}
function Toast ({ className, content }: Props): React.ReactElement<Props> {
return (
<div className={className}>
<p className='snackbar-content'>{content}</p>
</div>
);
}
export default styled(Toast)<{visible: boolean}>`
position: fixed;
display: ${({ visible }): string => visible ? 'block' : 'none'};
height: 40px;
text-align: center;
vertical-align: middle;
line-height: 7px;
top: 460px;
left: calc(50% - 50px);
&& {
margin: auto;
border-radius: 25px;
background: ${({ theme }): string => theme.highlightedAreaBackground};
}
`;

View File

@ -0,0 +1,41 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React, { useCallback, useState } from 'react';
import { ToastContext } from '..';
import Toast from './Toast';
interface ToastProviderProps {
children?: React.ReactNode;
}
const TOAST_TIMEOUT = 1500;
const ToastProvider = ({ children }: ToastProviderProps): React.ReactElement<ToastProviderProps> => {
const [content, setContent] = useState('');
const [visible, setVisible] = useState(false);
const show = useCallback((message: string): () => void => {
const timerId = setTimeout(() => setVisible(false), TOAST_TIMEOUT);
setContent(message);
setVisible(true);
return (): void => clearTimeout(timerId);
}, []);
return (
<ToastContext.Provider value={{ show }}>
{children}
<Toast
content={content}
visible={visible}
/>
</ToastContext.Provider>
);
};
export default ToastProvider;
ToastProvider.displayName = 'Toast';

View File

@ -17,6 +17,7 @@ const MediaContext = React.createContext<boolean>(false);
const MetadataReqContext = React.createContext<MetadataRequest[]>([]);
const SigningReqContext = React.createContext<SigningRequest[]>([]);
const ThemeSwitchContext = React.createContext<(theme: AvailableThemes) => void>(noop);
const ToastContext = React.createContext<({show: (message: string) => void})>({ show: noop });
export {
AccountContext,
@ -25,5 +26,6 @@ export {
MediaContext,
MetadataReqContext,
SigningReqContext,
ThemeSwitchContext
ThemeSwitchContext,
ToastContext
};

View File

@ -0,0 +1,10 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { useContext } from 'react';
import { ToastContext } from '../components/contexts';
export default function useToast (): {show: (message: string) => void} {
return useContext(ToastContext);
}

View File

@ -2917,6 +2917,7 @@ __metadata:
"@types/bn.js": ^4.11.6
"@types/enzyme": ^3.10.5
"@types/enzyme-adapter-react-16": ^1.0.6
"@types/react-copy-to-clipboard": ^4.3.0
"@types/react-dom": ^16.9.5
"@types/react-router": ^5.1.4
"@types/react-router-dom": ^5.1.3
@ -3503,6 +3504,15 @@ __metadata:
languageName: node
linkType: hard
"@types/react-copy-to-clipboard@npm:^4.3.0":
version: 4.3.0
resolution: "@types/react-copy-to-clipboard@npm:4.3.0"
dependencies:
"@types/react": "*"
checksum: 2/09816ba8a92fefbcc62abd4eea27881804c3ad0b70b52dcaf6717087ed95533d2afdbda3c78d5e3c5c41ebce13b19ca99aa710f3db258db393c2eada59d19c83
languageName: node
linkType: hard
"@types/react-dom@npm:^16.9.5":
version: 16.9.6
resolution: "@types/react-dom@npm:16.9.6"