Style component global cleanup (#518)

* ButtonArea

* List

* Box and Welcome screen

* Svg

* remove single used Title

* ValidateInput

* VerticalSpace

* View

* Export

* Forget

* DropdownAddress

* Accounts

* Request

* Authorize

* SelectParent

* Header

* Menu item spacing

* fix tests

* Apply suggestions from code review

Co-authored-by: Jaco Greeff <jacogr@gmail.com>

* Refactor Request into smaller components (#519)

* refactor into smaller components

* stray component

* Update packages/extension-ui/src/Popup/Signing/Request/SignArea.tsx

Co-authored-by: Jaco Greeff <jacogr@gmail.com>

* lint

Co-authored-by: Github Actions <action@github.com>
Co-authored-by: Jaco Greeff <jacogr@gmail.com>

Co-authored-by: Github Actions <action@github.com>
Co-authored-by: Jaco Greeff <jacogr@gmail.com>
This commit is contained in:
Thibaut Sardan 2020-10-23 20:26:14 +02:00 committed by GitHub
parent 824baee178
commit e7fb3651ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 682 additions and 555 deletions

View File

@ -1,6 +1,8 @@
// Copyright 2019-2020 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { ThemeProps } from '../../types';
import React, { useContext } from 'react';
import styled from 'styled-components';
@ -10,7 +12,11 @@ import { Header } from '../../partials';
import AccountsTree from './AccountsTree';
import AddAccount from './AddAccount';
export default function Accounts (): React.ReactElement {
interface Props extends ThemeProps {
className?: string;
}
function Accounts ({ className }: Props): React.ReactElement {
const { t } = useTranslation();
const { hierarchy } = useContext(AccountContext);
@ -25,14 +31,14 @@ export default function Accounts (): React.ReactElement {
showSettings
text={t<string>('Accounts')}
/>
<AccountsArea>
<div className={className}>
{hierarchy.map((json, index): React.ReactNode => (
<AccountsTree
{...json}
key={`${index}:${json.address}`}
/>
))}
</AccountsArea>
</div>
</>
)
}
@ -40,7 +46,7 @@ export default function Accounts (): React.ReactElement {
);
}
const AccountsArea = styled.div`
export default styled(Accounts)`
height: 100%;
overflow-y: scroll;
margin-top: -25px;

View File

@ -47,8 +47,9 @@ describe('Authorize', () => {
{ id: '2', request: { origin: 'abc' }, url: 'http://polkadot.pl' }
]);
expect(wrapper.find('AcceptButton').length).toBe(1);
expect(wrapper.find(Request).length).toBe(2);
expect(wrapper.find(Icon).length).toBe(2);
expect(wrapper.find(Request).at(1).find('.tab-info').text()).toBe('An application, self-identifying as abc is requesting access from http://polkadot.pl.');
expect(wrapper.find('button.acceptButton').length).toBe(1);
});
});

View File

@ -12,7 +12,7 @@ import { ActionBar, ActionContext, Button, Icon, Link, Warning } from '../../com
import useTranslation from '../../hooks/useTranslation';
import { approveAuthRequest, rejectAuthRequest } from '../../messaging';
interface Props {
interface Props extends ThemeProps {
authId: string;
className?: string;
isFirst: boolean;
@ -40,8 +40,8 @@ function Request ({ authId, className, isFirst, request: { origin }, url }: Prop
return (
<div className={className}>
<RequestInfo>
<Info>
<div className='requestInfo'>
<div className='info'>
<Icon
icon='X'
onClick={_onReject}
@ -57,59 +57,35 @@ function Request ({ authId, className, isFirst, request: { origin }, url }: Prop
</a>.
</Trans>
</div>
</Info>
</div>
{isFirst && (
<>
<RequestWarning>{t<string>('Only approve this request if you trust the application. Approving gives the application access to the addresses of your accounts.')}</RequestWarning>
<AcceptButton onClick={_onApprove}>{t<string>('Yes, allow this application access')}</AcceptButton>
<Warning className='warningMargin'>{t<string>('Only approve this request if you trust the application. Approving gives the application access to the addresses of your accounts.')}</Warning>
<Button
className='acceptButton'
onClick={_onApprove}
>
{t<string>('Yes, allow this application access')}
</Button>
</>
)}
<RejectButton>
<ActionBar className='rejectionButton'>
<Link
isDanger
onClick={_onReject}
>
Reject
</Link>
</RejectButton>
</RequestInfo>
</ActionBar>
</div>
</div>
);
}
const RequestInfo = styled.div`
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 8px;
background: ${({ theme }: ThemeProps): string => theme.highlightedAreaBackground};
`;
const Info = styled.div`
display: flex;
flex-direction: row;
`;
const AcceptButton = styled(Button)`
width: 90%;
margin: 25px auto 0;
`;
const RequestWarning = styled(Warning)`
margin: 24px 24px 0 1.45rem;
`;
AcceptButton.displayName = 'AcceptButton';
const RejectButton = styled(ActionBar)`
margin: 8px 0 15px 0;
text-decoration: underline;
`;
export default styled(Request)`
export default styled(Request)(({ theme }: Props) => `
.icon {
background: ${({ theme }: ThemeProps): string => theme.buttonBackgroundDanger};
background: ${theme.buttonBackgroundDanger};
color: white;
min-width: 18px;
width: 14px;
@ -128,7 +104,7 @@ export default styled(Request)`
.tab-name,
.tab-url {
color: ${({ theme }: ThemeProps): string => theme.textColor};
color: ${theme.textColor};
display: inline-block;
max-width: 20rem;
overflow: hidden;
@ -137,4 +113,31 @@ export default styled(Request)`
cursor: pointer;
text-decoration: underline;
}
`;
.requestInfo {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 8px;
background: ${theme.highlightedAreaBackground};
}
.info {
display: flex;
flex-direction: row;
}
.acceptButton {
width: 90%;
margin: 25px auto 0;
}
.warningMargin {
margin: 24px 24px 0 1.45rem;
}
.rejectionButton {
margin: 8px 0 15px 0;
text-decoration: underline;
}
`);

View File

@ -1,6 +1,8 @@
// Copyright 2019-2020 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { ThemeProps } from '../../types';
import React, { useContext } from 'react';
import styled from 'styled-components';
@ -9,36 +11,45 @@ import useTranslation from '../../hooks/useTranslation';
import { Header } from '../../partials';
import Request from './Request';
export default function Authorize (): React.ReactElement {
interface Props extends ThemeProps {
className?: string;
}
function Authorize ({ className = '' }: Props): React.ReactElement {
const { t } = useTranslation();
const requests = useContext(AuthorizeReqContext);
return (
<>
<Scroll isLastRequest={requests.length === 1}>
<div className={`${className} ${requests.length === 1 ? 'lastRequest' : ''}`}>
<Header text={t<string>('Authorize')} />
{requests.map(({ id, request, url }, index): React.ReactNode => (
<Request
authId={id}
className='request'
isFirst={index === 0}
key={id}
request={request}
url={url}
/>
))}
</Scroll>
</div>
</>
);
}
const Scroll = styled.div<{isLastRequest: boolean}>`
overflow-y: ${({ isLastRequest }): string => isLastRequest ? 'hidden' : 'auto'};
export default styled(Authorize)`
overflow-y: auto;
&.lastRequest {
overflow: hidden;
}
&& {
padding: 0;
}
${Request} {
.request {
padding: 0 24px;
}
`;

View File

@ -101,7 +101,7 @@ describe('Create Account', () => {
it('after typing less than 3 characters into name input, password input is not visible', async () => {
await enterName('ab');
expect(wrapper.find(InputWithLabel).find('[data-input-name]').find(Input).prop('withError')).toBe(true);
expect(wrapper.find('ErrorMessage').first().text()).toBe('Account name is too short');
expect(wrapper.find('span.error').first().text()).toBe('Account name is too short');
expect(wrapper.find(InputWithLabel).find('[data-input-password]').find(Input)).toHaveLength(1);
expect(wrapper.find(InputWithLabel).find('[data-input-repeat-password]')).toHaveLength(0);
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(true);
@ -124,7 +124,7 @@ describe('Create Account', () => {
it('password shorter than 6 characters should be not valid', async () => {
await enterName('abc').then(password('abcde'));
expect(wrapper.find(InputWithLabel).find('[data-input-password]').find(Input).prop('withError')).toBe(true);
expect(wrapper.find('ErrorMessage').text()).toBe('Password is too short');
expect(wrapper.find('span.error').text()).toBe('Password is too short');
expect(wrapper.find(InputWithLabel).find('[data-input-password]').find(Input)).toHaveLength(1);
expect(wrapper.find(InputWithLabel).find('[data-input-repeat-password]')).toHaveLength(0);
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(true);
@ -132,14 +132,14 @@ describe('Create Account', () => {
it('submit button is not visible until both passwords are equal', async () => {
await enterName('abc').then(password('abcdef')).then(repeat('abcdeg'));
expect(wrapper.find('ErrorMessage').text()).toBe('Passwords do not match');
expect(wrapper.find('span.error').text()).toBe('Passwords do not match');
expect(wrapper.find(InputWithLabel).find('[data-input-repeat-password]').find(Input).prop('withError')).toBe(true);
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(true);
});
it('submit button is visible when both passwords are equal', async () => {
await enterName('abc').then(password('abcdef')).then(repeat('abcdef'));
expect(wrapper.find('ErrorMessage')).toHaveLength(0);
expect(wrapper.find('span.error')).toHaveLength(0);
expect(wrapper.find(InputWithLabel).find('[data-input-repeat-password]').find(Input).prop('withError')).toBe(false);
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(false);
});
@ -170,7 +170,7 @@ describe('Create Account', () => {
it('first password changes - button is not disabled', async () => {
await type(wrapper.find('input[type="password"]').first(), 'aaaaaa');
expect(wrapper.find('ErrorMessage').text()).toBe('Passwords do not match');
expect(wrapper.find('span.error').text()).toBe('Passwords do not match');
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(true);
});

View File

@ -7,7 +7,7 @@ import React, { useCallback, useRef, useState } from 'react';
import styled from 'styled-components';
import arrow from '../../assets/arrow-down.svg';
import { Address, Svg } from '../../components';
import { Address } from '../../components';
import useOutsideClick from '../../hooks/useOutsideClick';
interface Props {
@ -36,10 +36,11 @@ function AddressDropdown ({ allAddresses, className, onSelect, selectedAddress,
>
<Address
address={selectedAddress}
className='address'
genesisHash={selectedGenesis}
/>
</div>
<HiddenOptions visible={isDropdownVisible}>
<div className={`dropdown ${isDropdownVisible ? 'visible' : ''}`}>
{allAddresses.map(([address, genesisHash]) => (
<div
data-parent-option
@ -48,39 +49,21 @@ function AddressDropdown ({ allAddresses, className, onSelect, selectedAddress,
>
<Address
address={address}
className='address'
genesisHash={genesisHash}
/>
</div>
))}
</HiddenOptions>
</div>
</div>
);
}
const HiddenOptions = styled.div<{ visible: boolean }>`
position: absolute;
visibility: ${({ visible }): string => (visible ? 'visible' : 'hidden')};
width: 510px;
z-index: 100;
background: ${({ theme }: ThemeProps): string => theme.bodyColor};
max-height: ${({ visible }): string => (visible ? '200px' : '0')};
overflow: scroll;
padding: 5px;
border: 1px solid ${({ theme }: ThemeProps): string => theme.boxBorderColor};
box-sizing: border-box;
border-radius: 4px;
margin-top: -8px;
& > div {
cursor: pointer;
}
`;
export default styled(AddressDropdown)(({ theme }: ThemeProps) => `
margin-bottom: 16px;
cursor: pointer;
& > div:first-child > ${Address}::after {
& > div:first-child > .address::after {
content: '';
position: absolute;
top: 66%;
@ -95,7 +78,31 @@ export default styled(AddressDropdown)(({ theme }: ThemeProps) => `
border: 1px solid ${theme.boxBorderColor};
}
${Address} ${Svg} {
.address .copyIcon {
visibility: hidden;
}
.dropdown {
position: absolute;
visibility: hidden;
width: 510px;
z-index: 100;
background: ${theme.bodyColor};
max-height: 0;
overflow: auto;
padding: 5px;
border: 1px solid ${theme.boxBorderColor};
box-sizing: border-box;
border-radius: 4px;
margin-top: -8px;
&.visible{
visibility: visible;
max-height: 200px;
}
& > div {
cursor: pointer;
}
}
`);

View File

@ -1,6 +1,8 @@
// Copyright 2019-2020 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { ThemeProps } from '../../types';
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import styled from 'styled-components';
import { assert } from '@polkadot/util';
@ -12,14 +14,15 @@ import { nextDerivationPath } from '../../util/nextDerivationPath';
import AddressDropdown from './AddressDropdown';
import DerivationPath from './DerivationPath';
interface Props {
interface Props extends ThemeProps {
className?: string;
isLocked?: boolean;
parentAddress: string;
parentGenesis: string | null;
onDerivationConfirmed: (derivation: { account: { address: string; suri: string }; parentPassword: string }) => void;
}
export function SelectParent ({ isLocked, onDerivationConfirmed, parentAddress, parentGenesis }: Props): React.ReactElement<Props> {
function SelectParent ({ className, isLocked, onDerivationConfirmed, parentAddress, parentGenesis }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const onAction = useContext(ActionContext);
const [isBusy, setIsBusy] = useState(false);
@ -84,58 +87,60 @@ export function SelectParent ({ isLocked, onDerivationConfirmed, parentAddress,
setParentPassword('');
setIsProperParentPassword(false);
// eslint-disable-next-line no-unused-expressions
passwordInputRef.current?.querySelector('input')?.focus();
}, [_onParentPasswordEnter]);
return (
<>
{!isLocked && (
<CheckboxWithSmallerMargins
checked={shouldAccountBeDerived}
label={t<string>('Derive new account from existing')}
onChange={setShouldAccountBeDerived}
/>
)}
<DisableableArea isDisabled={!shouldAccountBeDerived}>
{isLocked
? (
<Address
address={parentAddress}
genesisHash={parentGenesis}
/>
)
: (
<Label label={t<string>('Choose Parent Account:')}>
<AddressDropdown
allAddresses={allAddresses}
onSelect={_onParentChange}
selectedAddress={parentAddress}
selectedGenesis={parentGenesis}
/>
</Label>
)
}
<div ref={passwordInputRef}>
<InputWithLabel
data-export-password
isError={!isProperParentPassword}
isFocused
label={t<string>('enter the password for the account you want to derive from')}
onChange={_onParentPasswordEnter}
type='password'
value={parentPassword}
/>
</div>
{isProperParentPassword && (
<DerivationPath
defaultPath={defaultPath}
onChange={setSuriPath}
parentAddress={parentAddress}
parentPassword={parentPassword}
<div className={className}>
{!isLocked && (
<Checkbox
checked={shouldAccountBeDerived}
className='smallerMargin'
label={t<string>('Derive new account from existing')}
onChange={setShouldAccountBeDerived}
/>
)}
</DisableableArea>
<div className={`disableArea ${shouldAccountBeDerived ? '' : 'disabled'}`}>
{isLocked
? (
<Address
address={parentAddress}
genesisHash={parentGenesis}
/>
)
: (
<Label label={t<string>('Choose Parent Account:')}>
<AddressDropdown
allAddresses={allAddresses}
onSelect={_onParentChange}
selectedAddress={parentAddress}
selectedGenesis={parentGenesis}
/>
</Label>
)
}
<div ref={passwordInputRef}>
<InputWithLabel
data-export-password
isError={!isProperParentPassword}
isFocused
label={t<string>('enter the password for the account you want to derive from')}
onChange={_onParentPasswordEnter}
type='password'
value={parentPassword}
/>
</div>
{isProperParentPassword && (
<DerivationPath
defaultPath={defaultPath}
onChange={setSuriPath}
parentAddress={parentAddress}
parentPassword={parentPassword}
/>
)}
</div>
</div>
<VerticalSpace/>
<ButtonArea>
{shouldAccountBeDerived
@ -163,12 +168,19 @@ export function SelectParent ({ isLocked, onDerivationConfirmed, parentAddress,
);
}
const CheckboxWithSmallerMargins = styled(Checkbox)`
margin-top: 0;
margin-bottom: 10px;
`;
export default styled(SelectParent)`
const DisableableArea = styled.div<{ isDisabled: boolean }>`
opacity: ${({ isDisabled }): string => isDisabled ? '0.2' : '1'};
${({ isDisabled }): string | false => isDisabled && 'pointer-events: none'};
.smallerMargin {
margin-top: 0;
margin-bottom: 10px;
}
.disableArea {
opacity: 1;
.disabled {
opacity: 0.2;
pointer-events: none;
}
}
`;

View File

@ -8,7 +8,7 @@ import { AccountContext, ActionContext, Address, BackButton, ButtonArea, NextSte
import useTranslation from '../../hooks/useTranslation';
import { deriveAccount } from '../../messaging';
import { HeaderWithSteps, Name, Password } from '../../partials';
import { SelectParent } from './SelectParent';
import SelectParent from './SelectParent';
interface Props {
isLocked?: boolean;

View File

@ -1,6 +1,8 @@
// Copyright 2019-2020 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { ThemeProps } from '../types';
import React, { useCallback, useContext, useState } from 'react';
import { RouteComponentProps, withRouter } from 'react-router';
@ -12,9 +14,11 @@ import styled from 'styled-components';
const MIN_LENGTH = 6;
type Props = RouteComponentProps<{ address: string }>;
interface Props extends RouteComponentProps<{address: string}>, ThemeProps {
className?: string;
}
function Export ({ match: { params: { address } } }: Props): React.ReactElement<Props> {
function Export ({ className, match: { params: { address } } }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const onAction = useContext(ActionContext);
const [isBusy, setIsBusy] = useState(false);
@ -58,10 +62,15 @@ function Export ({ match: { params: { address } } }: Props): React.ReactElement<
showBackArrow
text={t<string>('Export account')}
/>
<div>
<div className={className}>
<Address address={address}>
<MovedWarning danger>{t<string>("You are exporting your account. Keep it safe and don't share it with anyone.")}</MovedWarning>
<ActionArea>
<Warning
className='movedWarning'
isDanger
>
{t<string>("You are exporting your account. Keep it safe and don't share it with anyone.")}
</Warning>
<div className='actionArea'>
<InputWithLabel
data-export-password
disabled={isBusy}
@ -80,34 +89,34 @@ function Export ({ match: { params: { address } } }: Props): React.ReactElement<
>
{t<string>('I want to export this account')}
</Button>
<CancelButton>
<ActionBar className='withMarginTop'>
<ActionText
className='center'
onClick={_goHome}
text={t<string>('Cancel')}
/>
</CancelButton>
</ActionArea>
</ActionBar>
</div>
</Address>
</div>
</>
);
}
const MovedWarning = styled(Warning)`
margin-top: 8px;
`;
export default withRouter(styled(Export)`
.actionArea {
padding: 10px 24px;
}
const ActionArea = styled.div`
padding: 10px 24px;
`;
const CancelButton = styled(ActionBar)`
margin-top: 4px;
text-decoration: underline;
${ActionText} {
.center {
margin: auto;
}
`;
export default withRouter(Export);
.movedWarning {
margin-top: 8px;
}
.withMarginTop {
margin-top: 4px;
}
`);

View File

@ -1,5 +1,6 @@
// Copyright 2019-2020 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { ThemeProps } from '../types';
import React, { useCallback, useContext } from 'react';
import { RouteComponentProps, withRouter } from 'react-router';
@ -10,9 +11,11 @@ import useTranslation from '../hooks/useTranslation';
import { forgetAccount } from '../messaging';
import { Header } from '../partials';
type Props = RouteComponentProps<{ address: string }>;
interface Props extends RouteComponentProps<{ address: string }>, ThemeProps {
className?: string;
}
function Forget ({ match: { params: { address } } }: Props): React.ReactElement<Props> {
function Forget ({ className, match: { params: { address } } }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const onAction = useContext(ActionContext);
@ -35,43 +38,49 @@ function Forget ({ match: { params: { address } } }: Props): React.ReactElement<
showBackArrow
text={t<string>('Forget account')}
/>
<div>
<div className={className}>
<Address address={address}>
<MovedWarning isDanger>{t<string>('You are about to remove the account. This means that you will not be able to access it via this extension anymore. If you wish to recover it, you would need to use the seed.')}</MovedWarning>
<ActionArea>
<Warning
className='movedWarning'
isDanger
>
{t<string>('You are about to remove the account. This means that you will not be able to access it via this extension anymore. If you wish to recover it, you would need to use the seed.')}
</Warning>
<div className='actionArea'>
<Button
isDanger
onClick={_onClick}
>
{t<string>('I want to forget this account')}
</Button>
<CancelButton>
<ActionBar className='withMarginTop'>
<ActionText
className='center'
onClick={_goHome}
text={t<string>('Cancel')}
/>
</CancelButton>
</ActionArea>
</ActionBar>
</div>
</Address>
</div>
</>
);
}
const MovedWarning = styled(Warning)`
margin-top: 8px;
`;
export default withRouter(styled(Forget)`
.actionArea {
padding: 10px 24px;
}
const ActionArea = styled.div`
padding: 10px 24px;
`;
const CancelButton = styled(ActionBar)`
margin-top: 4px;
${ActionText} {
.center {
margin: auto;
}
`;
export default withRouter(Forget);
.movedWarning {
margin-top: 8px;
}
.withMarginTop {
margin-top: 4px;
}
`);

View File

@ -70,8 +70,9 @@ export default function Import (): React.ReactElement {
return (
<>
<HeaderWithSmallerMargin
<Header
showBackArrow
smallMargin
text={t<string>('Import account')}
/>
<div>
@ -112,10 +113,6 @@ export default function Import (): React.ReactElement {
);
}
const HeaderWithSmallerMargin = styled(Header)`
margin-bottom: 15px;
`;
const SeedInput = styled(TextAreaWithLabel)`
margin-bottom: 16px;
textarea {

View File

@ -1,259 +0,0 @@
// Copyright 2019-2020 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { ExtrinsicPayload } from '@polkadot/types/interfaces';
import { AccountJson, RequestSign } from '@polkadot/extension-base/background/types';
import { SignerPayloadJSON, SignerPayloadRaw } from '@polkadot/types/types';
import React, { useCallback, useEffect, useContext, useState } from 'react';
import { PASSWORD_EXPIRY_MIN } from '@polkadot/extension-base/defaults';
import { TypeRegistry } from '@polkadot/types';
import { ActionBar, ActionContext, Address, Button, ButtonArea, Checkbox, Link, VerticalSpace } from '../../components';
import useTranslation from '../../hooks/useTranslation';
import { approveSignPassword, approveSignSignature, cancelSignRequest, isSignLocked } from '../../messaging';
import Bytes from './Bytes';
import Extrinsic from './Extrinsic';
import Qr from './Qr';
import Unlock from './Unlock';
import styled from 'styled-components';
interface Props {
account: AccountJson;
buttonText: string;
isFirst?: boolean;
request: RequestSign;
signId: string;
url: string;
}
interface Data {
hexBytes: string | null;
payload: ExtrinsicPayload | null;
}
// keep it global, we can and will re-use this across requests
const registry = new TypeRegistry();
function isRawPayload (payload: SignerPayloadJSON | SignerPayloadRaw): payload is SignerPayloadRaw {
return !!(payload as SignerPayloadRaw).data;
}
export default function Request ({ account: { isExternal }, buttonText, isFirst, request, signId, url }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const onAction = useContext(ActionContext);
const [{ hexBytes, payload }, setData] = useState<Data>({ hexBytes: null, payload: null });
const [error, setError] = useState<string | null>(null);
const [isBusy, setIsBusy] = useState(false);
const [isLocked, setIsLocked] = useState<boolean | null>(null);
const [savePass, setSavePass] = useState(false);
const [password, setPassword] = useState('');
useEffect(() => {
setIsLocked(null);
let timeout: number;
!isExternal && isSignLocked(signId)
.then(({ isLocked, remainingTime }) => {
setIsLocked(isLocked);
timeout = setTimeout(() => {
setIsLocked(true);
}, remainingTime);
// if the account was unlocked check the remember me
// automatically to prolong the unlock period
!isLocked && setSavePass(true);
})
.catch((error: Error) => console.error(error));
return () => { !!timeout && clearTimeout(timeout); };
}, [isExternal, signId]);
useEffect((): void => {
const payload = request.payload;
if (isRawPayload(payload)) {
setData({
hexBytes: payload.data,
payload: null
});
} else {
registry.setSignedExtensions(payload.signedExtensions);
setData({
hexBytes: null,
payload: registry.createType('ExtrinsicPayload', payload, { version: payload.version })
});
}
}, [request]);
const _onCancel = useCallback(
(): Promise<void> => cancelSignRequest(signId)
.then(() => onAction())
.catch((error: Error) => console.error(error)),
[onAction, signId]
);
const _onSign = useCallback(
(): Promise<void> => {
setIsBusy(true);
return approveSignPassword(signId, savePass, password)
.then((): void => {
setIsBusy(false);
onAction();
})
.catch((error: Error): void => {
setIsBusy(false);
setError(error.message);
console.error(error);
});
},
[onAction, password, savePass, signId]
);
const _onSignature = useCallback(
({ signature }: { signature: string }): Promise<void> =>
approveSignSignature(signId, signature)
.then(() => onAction())
.catch((error: Error): void => {
setError(error.message);
console.error(error);
}),
[onAction, signId]
);
const RememberPasswordCheckbox = () => (
<Checkbox
checked={savePass}
label={ isLocked
? t<string>(
'Remember my password for the next {{expiration}} minutes',
{ replace: { expiration: PASSWORD_EXPIRY_MIN } }
)
: t<string>(
'Extend the period without password by {{expiration}} minutes',
{ replace: { expiration: PASSWORD_EXPIRY_MIN } }
)
}
onChange={setSavePass}
/>
);
const SignButton = () => (
<>
{ isLocked && (
<Unlock
error={error}
isBusy={isBusy}
onSign={_onSign}
password={password}
setError={setError}
setPassword={setPassword}
/>
)}
<RememberPasswordCheckbox />
<Button
isBusy={isBusy}
isDisabled={(!!isLocked && !password) || !!error}
onClick={_onSign}
>
{buttonText}
</Button>
</>
);
if (payload !== null) {
const json = request.payload as SignerPayloadJSON;
return (
<>
<div>
<Address
address={json.address}
genesisHash={json.genesisHash}
isExternal={isExternal}
/>
</div>
{isExternal
? (
<Qr
onSignature={_onSignature}
payload={payload}
request={json}
/>
)
: (
<Extrinsic
isDecoded={true}
payload={payload}
request={json}
url={url}
/>
)
}
<SignArea>
{isFirst && !isExternal && (
<SignButton/>
)}
<CancelButton>
<Link
isDanger
onClick={_onCancel}
>
{t<string>('Cancel')}
</Link>
</CancelButton>
</SignArea>
</>
);
} else if (hexBytes !== null) {
const raw = request.payload as SignerPayloadRaw;
return (
<>
<div>
<Address
address={raw.address}
isExternal={isExternal}
/>
</div>
<Bytes
bytes={raw.data}
url={url}
/>
<VerticalSpace />
<SignArea>
{!isExternal && <SignButton/>}
<CancelButton>
<Link
isDanger
onClick={_onCancel}
>
{t<string>('Reject')}
</Link>
</CancelButton>
</SignArea>
</>
);
}
return null;
}
const SignArea = styled(ButtonArea)`
flex-direction: column;
padding: 6px 24px;
`;
const CancelButton = styled(ActionBar)`
margin-top: 4px;
margin-bottom: 4px;
text-decoration: underline;
a {
margin: auto;
}
`;
CancelButton.displayName = 'CancelButton';

View File

@ -0,0 +1,142 @@
// Copyright 2019-2020 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { PASSWORD_EXPIRY_MIN } from '@polkadot/extension-base/defaults';
import React, { useCallback, useContext, useEffect, useState } from 'react';
import styled from 'styled-components';
import { ActionBar, ActionContext, Button, ButtonArea, Checkbox, Link } from '../../../components';
import useTranslation from '../../../hooks/useTranslation';
import { approveSignPassword, cancelSignRequest, isSignLocked } from '../../../messaging';
import Unlock from '../Unlock';
interface Props {
buttonText: string;
className?: string;
error: string | null;
isExternal?: boolean;
isFirst: boolean;
setError: (value: string | null) => void;
signId: string;
}
function SignArea ({ buttonText, className, error, isExternal, isFirst, setError, signId } : Props): JSX.Element {
const [savePass, setSavePass] = useState(false);
const [isLocked, setIsLocked] = useState<boolean | null>(null);
const [password, setPassword] = useState('');
const [isBusy, setIsBusy] = useState(false);
const onAction = useContext(ActionContext);
const { t } = useTranslation();
useEffect(() => {
setIsLocked(null);
let timeout: number;
!isExternal && isSignLocked(signId)
.then(({ isLocked, remainingTime }) => {
setIsLocked(isLocked);
timeout = setTimeout(() => {
setIsLocked(true);
}, remainingTime);
// if the account was unlocked check the remember me
// automatically to prolong the unlock period
!isLocked && setSavePass(true);
})
.catch((error: Error) => console.error(error));
return () => { !!timeout && clearTimeout(timeout); };
}, [isExternal, signId]);
const _onSign = useCallback(
(): Promise<void> => {
setIsBusy(true);
return approveSignPassword(signId, savePass, password)
.then((): void => {
setIsBusy(false);
onAction();
})
.catch((error: Error): void => {
setIsBusy(false);
setError(error.message);
console.error(error);
});
},
[onAction, password, savePass, setError, setIsBusy, signId]
);
const _onCancel = useCallback(
(): Promise<void> => cancelSignRequest(signId)
.then(() => onAction())
.catch((error: Error) => console.error(error)),
[onAction, signId]
);
const RememberPasswordCheckbox = () => (
<Checkbox
checked={savePass}
label={ isLocked
? t<string>(
'Remember my password for the next {{expiration}} minutes',
{ replace: { expiration: PASSWORD_EXPIRY_MIN } }
)
: t<string>(
'Extend the period without password by {{expiration}} minutes',
{ replace: { expiration: PASSWORD_EXPIRY_MIN } }
)
}
onChange={setSavePass}
/>
);
return (
<ButtonArea className={className}>
{isFirst && !isExternal && (
<>
{ isLocked && (
<Unlock
error={error}
isBusy={isBusy}
onSign={_onSign}
password={password}
setError={setError}
setPassword={setPassword}
/>
)}
<RememberPasswordCheckbox />
<Button
isBusy={isBusy}
isDisabled={(!!isLocked && !password) || !!error}
onClick={_onSign}
>
{buttonText}
</Button>
</>
)}
<ActionBar className='cancelButton'>
<Link
isDanger
onClick={_onCancel}
>
{t<string>('Cancel')}
</Link>
</ActionBar>
</ButtonArea>
);
}
export default styled(SignArea)`
flex-direction: column;
padding: 6px 24px;
.cancelButton {
margin-top: 4px;
margin-bottom: 4px;
text-decoration: underline;
a {
margin: auto;
}
}
`;

View File

@ -0,0 +1,141 @@
// Copyright 2019-2020 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { ExtrinsicPayload } from '@polkadot/types/interfaces';
import { AccountJson, RequestSign } from '@polkadot/extension-base/background/types';
import { TypeRegistry } from '@polkadot/types';
import { SignerPayloadJSON, SignerPayloadRaw } from '@polkadot/types/types';
import React, { useCallback, useEffect, useContext, useState } from 'react';
import { ActionContext, Address, VerticalSpace } from '../../../components';
import { approveSignSignature } from '../../../messaging';
import Bytes from '../Bytes';
import Extrinsic from '../Extrinsic';
import Qr from '../Qr';
import SignArea from './SignArea';
interface Props {
account: AccountJson;
buttonText: string;
isFirst: boolean;
request: RequestSign;
signId: string;
url: string;
}
interface Data {
hexBytes: string | null;
payload: ExtrinsicPayload | null;
}
// keep it global, we can and will re-use this across requests
const registry = new TypeRegistry();
function isRawPayload (payload: SignerPayloadJSON | SignerPayloadRaw): payload is SignerPayloadRaw {
return !!(payload as SignerPayloadRaw).data;
}
export default function Request ({ account: { isExternal }, buttonText, isFirst, request, signId, url }: Props): React.ReactElement<Props> | null {
const onAction = useContext(ActionContext);
const [{ hexBytes, payload }, setData] = useState<Data>({ hexBytes: null, payload: null });
const [error, setError] = useState<string | null>(null);
useEffect((): void => {
const payload = request.payload;
if (isRawPayload(payload)) {
setData({
hexBytes: payload.data,
payload: null
});
} else {
registry.setSignedExtensions(payload.signedExtensions);
setData({
hexBytes: null,
payload: registry.createType('ExtrinsicPayload', payload, { version: payload.version })
});
}
}, [request]);
const _onSignature = useCallback(
({ signature }: { signature: string }): Promise<void> =>
approveSignSignature(signId, signature)
.then(() => onAction())
.catch((error: Error): void => {
setError(error.message);
console.error(error);
}),
[onAction, signId]
);
if (payload !== null) {
const json = request.payload as SignerPayloadJSON;
return (
<>
<div>
<Address
address={json.address}
genesisHash={json.genesisHash}
isExternal={isExternal}
/>
</div>
{isExternal
? (
<Qr
onSignature={_onSignature}
payload={payload}
request={json}
/>
)
: (
<Extrinsic
isDecoded={true}
payload={payload}
request={json}
url={url}
/>
)
}
<SignArea
buttonText={buttonText}
error={error}
isExternal={isExternal}
isFirst={isFirst}
setError={setError}
signId={signId}
/>
</>
);
} else if (hexBytes !== null) {
const raw = request.payload as SignerPayloadRaw;
return (
<>
<div>
<Address
address={raw.address}
isExternal={isExternal}
/>
</div>
<Bytes
bytes={raw.data}
url={url}
/>
<VerticalSpace />
<SignArea
buttonText={buttonText}
error={error}
isExternal={isExternal}
isFirst={isFirst}
setError={setError}
signId={signId}
/>
</>
);
}
return null;
}

View File

@ -1,7 +1,7 @@
// Copyright 2019-2020 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { ThemeProps as Props } from '../types';
import { ThemeProps } from '../types';
import React, { useContext } from 'react';
import styled from 'styled-components';
@ -10,7 +10,11 @@ import { ActionContext, Box, Button, ButtonArea, List, VerticalSpace } from '../
import useTranslation from '../hooks/useTranslation';
import { Header } from '../partials';
export default function Welcome (): React.ReactElement<Props> {
interface Props extends ThemeProps {
className?: string;
}
const Welcome = function ({ className }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const onAction = useContext(ActionContext);
@ -22,31 +26,29 @@ export default function Welcome (): React.ReactElement<Props> {
return (
<>
<Header text={t<string>('Welcome')} />
<Note>{t<string>('Before we start, just a couple of notes regarding use:')}</Note>
<TextBox>
<List>
<li>{t<string>('We do not send any clicks, pageviews or events to a central server')}</li>
<li>{t<string>('We do not use any trackers or analytics')}</li>
<li>{t<string>("We don't collect keys, addresses or any information - your information never leaves this machine")}</li>
</List>
</TextBox>
<Note>{t<string>('... we are not in the information collection business (even anonymized).')}</Note>
<div className={className}>
<p>{t<string>('Before we start, just a couple of notes regarding use:')}</p>
<Box>
<List>
<li>{t<string>('We do not send any clicks, pageviews or events to a central server')}</li>
<li>{t<string>('We do not use any trackers or analytics')}</li>
<li>{t<string>("We don't collect keys, addresses or any information - your information never leaves this machine")}</li>
</List>
</Box>
<p>{t<string>('... we are not in the information collection business (even anonymized).')}</p>
</div>
<VerticalSpace />
<ButtonArea>
<Button onClick={_onClick}>{t<string>('Understood, let me continue')}</Button>
</ButtonArea>
</>
);
}
};
const Note = styled.p(({ theme }: Props) => `
color: ${theme.subTextColor};
margin-bottom: 6px;
margin-top: 0;
`);
const TextBox = styled(Box)(({ theme }: Props) => `
border: 1px solid ${theme.inputBorderColor};
color: ${theme.subTextColor};
margin: 0.75rem 24px;
export default styled(Welcome)(({ theme }: Props) => `
p {
color: ${theme.subTextColor};
margin-bottom: 6px;
margin-top: 0;
}
`);

View File

@ -186,6 +186,7 @@ function Address ({ actions, address, children, className, genesisHash, isExtern
</div>
<CopyToClipboard text={(formatted && formatted) || ''} >
<FontAwesomeIcon
className='copyIcon'
icon={faCopy}
onClick={_onCopy}
size='sm'

View File

@ -23,12 +23,12 @@ function Box ({ banner, children, className }: Props): React.ReactElement<Props>
export default styled(Box)(({ theme }: ThemeProps) => `
background: ${theme.readonlyInputBackground};
border: none;
border: 1px solid ${theme.inputBorderColor};
border-radius: ${theme.borderRadius};
color: ${theme.textColor};
color: ${theme.subTextColor};
font-family: ${theme.fontFamily};
font-size: ${theme.fontSize};
margin: ${theme.boxMargin};
margin: 0.75rem 24px;
padding: ${theme.boxPadding};
position: relative;

View File

@ -3,22 +3,32 @@
import { ThemeProps } from '../types';
import React from 'react';
import styled from 'styled-components';
import Button from './Button';
export default styled.div(({ theme }: ThemeProps) => `
interface Props extends ThemeProps {
className?: string;
children: React.ReactNode;
}
const ButtonArea = function ({ children, className }: Props) {
return (
<div className={className}>
{children}
</div>
);
};
export default styled(ButtonArea)(({ theme }: ThemeProps) => `
display: flex;
flex-direction: row;
background: ${theme.highlightedAreaBackground};
border-top: 1px solid ${theme.inputBorderColor};
padding: 12px 24px;
margin-left: 0;
margin-right: 0;
&&& {
margin-left: 0;
margin-right: 0;
}
& > ${Button}:not(:last-of-type) {
& > button:not(:last-of-type) {
margin-right: 8px;
}
`);

View File

@ -1,11 +1,24 @@
// Copyright 2019-2020 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import { ThemeProps } from '../types';
import styled from 'styled-components';
export default styled.ul(({ theme }: ThemeProps) => `
interface Props extends ThemeProps {
className?: string;
children: React.ReactNode;
}
const List = ({ children, className }: Props) => (
<ul className={className}>
{children}
</ul>
);
export default styled(List)(({ theme }: ThemeProps) => `
list-style: none;
padding-inline-start: 10px;
padding-inline-end: 10px;

View File

@ -0,0 +1,40 @@
// Copyright 2019-2020 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { ThemeProps } from '../types';
import React from 'react';
import styled from 'styled-components';
interface Props {
children: React.ReactNode;
className?: string;
}
function Main ({ children, className }: Props): React.ReactElement<Props> {
return (
<main className={className}>
{children}
</main>
);
}
export default styled(Main)(({ theme }: ThemeProps) => `
display: flex;
flex-direction: column;
height: 100%;
background: ${theme.background};
color: ${theme.textColor};
font-size: ${theme.fontSize};
line-height: ${theme.lineHeight};
border: 1px solid ${theme.inputBorderColor};
* {
font-family: ${theme.fontFamily};
}
> * {
padding-left: 24px;
padding-right: 24px;
}
`);

View File

@ -4,9 +4,9 @@
import React from 'react';
import styled from 'styled-components';
import Title from './Title';
import { ThemeProps } from '../types';
interface Props {
interface Props extends ThemeProps {
children: React.ReactNode;
className?: string;
noBorder?: boolean;
@ -17,23 +17,27 @@ function MenuItem ({ children, className = '', title }: Props): React.ReactEleme
return (
<div className={className}>
{title && (
<Title className='itemTitle'>{title}</Title>
<div className='itemTitle'>{title}</div>
)}
{children}
</div>
);
}
export default styled(MenuItem)`
export default styled(MenuItem)(({ theme }: ThemeProps) => `
min-width: 13rem;
padding: 0 16px;
max-width: 100%;
> .itemTitle {
margin: 0;
}
&+& {
margin-top: 16px;
}
`;
margin: 0;
width: 100%;
font-weight: 800;
font-size: 10px;
line-height: 14px;
letter-spacing: 0.04em;
text-transform: uppercase;
color: ${theme.textColor};
opacity: 0.65;
}
`);

View File

@ -1,15 +1,20 @@
// Copyright 2019-2020 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import { ThemeProps } from '../types';
import styled from 'styled-components';
interface Props {
interface Props extends ThemeProps {
className?: string;
src: string;
}
export default styled.span<Props>(({ src, theme }: Props & ThemeProps) => `
const Svg = ({ className }: Props) => <span className={className} />;
export default styled(Svg)(({ src, theme }: Props) => `
background: ${theme.textColor};
display: inline-block;
mask: url(${src});

View File

@ -1,19 +0,0 @@
// Copyright 2019-2020 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import { ThemeProps } from '../types';
import styled from 'styled-components';
export default styled.div(({ theme }: ThemeProps) => `
width: 100%;
margin-bottom: 8px;
margin-top: 18px;
font-weight: 800;
font-size: 10px;
line-height: 14px;
letter-spacing: 0.04em;
text-transform: uppercase;
color: ${theme.textColor};
opacity: 0.65;
`);

View File

@ -50,19 +50,17 @@ function ValidatedInput<T extends Record<string, unknown>> ({ className, compone
onChange={setValue}
value={value}
/>
{Result.isError(validationResult) && <ErrorMessage>{validationResult.error.errorDescription}</ErrorMessage>}
{Result.isError(validationResult) && <span className='error'>{validationResult.error.errorDescription}</span>}
</div>
);
}
const ErrorMessage = styled.span`
display: block;
margin-top: -10px;
font-size: ${({ theme }: ThemeProps): string => theme.labelFontSize};
line-height: ${({ theme }: ThemeProps): string => theme.labelLineHeight};
color: ${({ theme }: ThemeProps): string => theme.errorColor};
`;
ErrorMessage.displayName = 'ErrorMessage';
export default ValidatedInput;
export default styled(ValidatedInput)(({ theme }: ThemeProps) => `
.error {
display: block;
margin-top: -10px;
font-size: ${theme.labelFontSize};
line-height: ${theme.labelLineHeight};
color: ${theme.errorColor};
}
`);

View File

@ -1,8 +0,0 @@
// Copyright 2019-2020 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import styled from 'styled-components';
export default styled.div`
height: 100%;
`;

View File

@ -0,0 +1,18 @@
// Copyright 2019-2020 @polkadot/extension-ui authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import styled from 'styled-components';
interface Props {
className?: string;
}
function VerticalSpace ({ className }: Props): React.ReactElement<Props> {
return <div className={className}/>;
}
export default styled(VerticalSpace)`
height: 100%;
`;

View File

@ -4,10 +4,10 @@
import { ThemeProps } from '../types';
import React, { useState } from 'react';
import styled, { ThemeProvider, createGlobalStyle } from 'styled-components';
import { ThemeProvider, createGlobalStyle } from 'styled-components';
// FIXME We should not import from index when this one is imported there as well
import { AvailableThemes, chooseTheme, themes, ThemeSwitchContext } from '.';
import { AvailableThemes, chooseTheme, Main, themes, ThemeSwitchContext } from '.';
interface Props {
children: React.ReactNode;
@ -49,24 +49,4 @@ const BodyTheme = createGlobalStyle<ThemeProps>`
}
`;
const Main = styled.main`
display: flex;
flex-direction: column;
height: 100%;
background: ${({ theme }: ThemeProps): string => theme.background};
color: ${({ theme }: ThemeProps): string => theme.textColor};
font-size: ${({ theme }: ThemeProps): string => theme.fontSize};
line-height: ${({ theme }: ThemeProps): string => theme.lineHeight};
border: 1px solid ${({ theme }: ThemeProps): string => theme.inputBorderColor};
* {
font-family: ${({ theme }: ThemeProps): string => theme.fontFamily};
}
> * {
padding-left: 24px;
padding-right: 24px;
}
`;
export default View;

View File

@ -21,6 +21,7 @@ export { default as Label } from './Label';
export { default as Link } from './Link';
export { default as List } from './List';
export { default as Loading } from './Loading';
export { default as Main } from './Main';
export { default as Menu } from './Menu';
export { default as MenuDivider } from './MenuDivider';
export { default as MenuItem } from './MenuItem';
@ -31,7 +32,6 @@ export { default as Svg } from './Svg';
export { default as Switch } from './Switch';
export { default as Table } from './Table';
export { default as TextAreaWithLabel } from './TextAreaWithLabel';
export { default as Title } from './Title';
export { default as ValidatedInput } from './ValidatedInput';
export { default as VerticalSpace } from './VerticalSpace';
export { default as View } from './View';

View File

@ -20,10 +20,11 @@ interface Props extends ThemeProps {
showAdd?: boolean;
showBackArrow?: boolean;
showSettings?: boolean;
smallMargin?: boolean;
text?: React.ReactNode;
}
function Header ({ children, className, showAdd, showBackArrow, showSettings, text }: Props): React.ReactElement<Props> {
function Header ({ children, className = '', showAdd, showBackArrow, showSettings, smallMargin = false, text }: Props): React.ReactElement<Props> {
const [isAddOpen, setShowAdd] = useState(false);
const [isSettingsOpen, setShowSettings] = useState(false);
const addRef = useRef(null);
@ -48,7 +49,7 @@ function Header ({ children, className, showAdd, showBackArrow, showSettings, te
);
return (
<div className={className}>
<div className={`${className} ${smallMargin ? 'smallMargin' : ''}`}>
<div className='container'>
<div className='branding'>
{showBackArrow
@ -197,6 +198,9 @@ export default React.memo(styled(Header)(({ theme }: Props) => `
&:visited {
color: ${theme.labelColor};
}
}
&.smallMargin {
margin-bottom: 15px;
}
`));