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:
parent
824baee178
commit
e7fb3651ab
@ -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;
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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;
|
||||
}
|
||||
`);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
`;
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
`);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
`);
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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';
|
||||
142
packages/extension-ui/src/Popup/Signing/Request/SignArea.tsx
Normal file
142
packages/extension-ui/src/Popup/Signing/Request/SignArea.tsx
Normal 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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
141
packages/extension-ui/src/Popup/Signing/Request/index.tsx
Normal file
141
packages/extension-ui/src/Popup/Signing/Request/index.tsx
Normal 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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
`);
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
`);
|
||||
|
||||
@ -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;
|
||||
|
||||
40
packages/extension-ui/src/components/Main.tsx
Normal file
40
packages/extension-ui/src/components/Main.tsx
Normal 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;
|
||||
}
|
||||
`);
|
||||
@ -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;
|
||||
}
|
||||
`);
|
||||
|
||||
@ -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});
|
||||
|
||||
@ -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;
|
||||
`);
|
||||
@ -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};
|
||||
}
|
||||
`);
|
||||
|
||||
@ -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%;
|
||||
`;
|
||||
18
packages/extension-ui/src/components/VerticalSpace.tsx
Normal file
18
packages/extension-ui/src/components/VerticalSpace.tsx
Normal 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%;
|
||||
`;
|
||||
@ -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;
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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;
|
||||
}
|
||||
`));
|
||||
|
||||
Loading…
Reference in New Issue
Block a user