* Inject theme using styled-components' ThemeProvider (#3)

* Apply colors from design, styles refactoring (#5)

* Add new tip and accounts screen without accounts (#7)

* Sync with upstream (#10)

* Use Nunito font (#11)

* Create account first page (#14)

* Customizable identicon (#13)

* Inline buttons (#16)

* Welcome screen styling (#17)

* Account creation second step (#15)

* Add new header (#20)

* change welcome screen (#19)

* Account screen (#18)

* Update from upstream (#21)

* Window title (#25)

* New authorize (#24)

* Refactor and remove unnecessary stuff from theme (#22)

* refactor add  accounts screen (#29)

* Export and forget screens, change general padding to 24px (#27)

* Signing screen (#26)

* Import account screens (#31)

* Change header with settings (#28)

* Refactor button and mnemonic (#30)

* Restyle checkbox (#33)

* Remove opacity from background; add borders (#32)

* Code style polishing (#34)

* Move fileMock to __mocks__ (#35)

* Add light theme (#36)

* Small design changes (#38)

* Change background color (#39)

* Sync with upstream (#41)

* Update screenshots in readme

* [CI Skip]  0.14.0-beta.7

skip-checks: true

* fix error for display menu (#42)

* fix display of authorization (#43)

* firefox scrol (#45)

* small change for rename account (#44)

* Sync with upstream

* Print and back button (#48)

* delete print button and change cancel to back button

* Set body color (#49)

* Fix the fix of account rename (#52)

* Update generic sign screen (#47)

* Improve UI/UX design (#53)

* [CI Skip]  0.14.0-beta.10

skip-checks: true

* Improve QR code recognition on dark theme (#54)

* Add proper tx/sign texts and headers (#55)

Co-authored-by: Kamil Dąbrowski <kamil@ethworks.io>
This commit is contained in:
Ivan Rukhavets
2019-12-20 10:06:43 +01:00
committed by Jaco Greeff
co-authored by Kamil Dąbrowski
parent cb13202da9
commit 85db56cd0f
94 changed files with 3040 additions and 866 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ As it stands, it does one thing: it _only_ manages accounts and allows the signi
- On Chrome, install via [Chrome web store](https://chrome.google.com/webstore/detail/polkadot%7Bjs%7D-extension/mopnmbcafieddcagagdcbnhejhlodfdd)
- On Firefox, install via [Firefox add-ons](https://addons.mozilla.org/en-US/firefox/addon/polkadot-js-extension/)
![interface screenshots](docs/concat-overview.jpg)
![interface screenshots](docs/concat-overview.png)
## FAQ
+2
View File
@@ -0,0 +1,2 @@
// eslint-disable-line
module.exports = '';
Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

+1 -1
View File
@@ -6,7 +6,7 @@ module.exports = Object.assign({}, config, {
moduleNameMapper: {
'@polkadot/extension-(chains|dapp|inject|ui)(.*)$': '<rootDir>/packages/extension-$1/src/$2',
'@polkadot/extension(.*)$': '<rootDir>/packages/extension/src/$1',
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 'empty/object',
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/__mocks__/fileMock.js',
'\\.(css|less)$': 'empty/object'
},
modulePathIgnorePatterns: [
@@ -4,11 +4,12 @@
import Adapter from 'enzyme-adapter-react-16';
import { configure, mount, ReactWrapper } from 'enzyme';
import { Link } from '@polkadot/extension-ui/components';
import { themes, Theme } from '@polkadot/extension-ui/components';
import { MemoryRouter } from 'react-router';
import React from 'react';
import Account from './Account';
import { ThemeProvider } from 'styled-components';
configure({ adapter: new Adapter() });
@@ -16,27 +17,32 @@ describe('Account component', () => {
let wrapper: ReactWrapper;
const VALID_ADDRESS = 'HjoBp62cvsWDA3vtNMWxz6c9q13ReEHi9UGHK7JbZweH5g5';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mountAccountComponent = (additionalAccountProperties: any): ReactWrapper => mount(
const mountAccountComponent = (additionalAccountProperties: any, theme: Theme = themes.dark): ReactWrapper => mount(
<MemoryRouter>
<Account
{...{ address: VALID_ADDRESS, ...additionalAccountProperties }}>
</Account>
<ThemeProvider theme={theme}>
<Account
{...{ address: VALID_ADDRESS, ...additionalAccountProperties }}
>
</Account>
</ThemeProvider>
</MemoryRouter>);
it('shows Export option if account is not external', () => {
wrapper = mountAccountComponent({ isExternal: false });
wrapper.find('Details').simulate('click');
expect(wrapper.find(Link).length).toBe(3);
expect(wrapper.find(Link).at(0).text()).toContain('Forget');
expect(wrapper.find(Link).at(1).text()).toContain('Export');
expect(wrapper.find(Link).at(2).text()).toContain('Edit');
expect(wrapper.find('MenuItem').length).toBe(3);
expect(wrapper.find('MenuItem').at(0).text()).toBe('Rename');
expect(wrapper.find('MenuItem').at(1).text()).toBe('Export Account');
expect(wrapper.find('MenuItem').at(2).text()).toBe('Forget Account');
});
it('does not show Export option if account is external', () => {
wrapper = mountAccountComponent({ isExternal: true });
wrapper.find('Details').simulate('click');
expect(wrapper.find(Link).length).toBe(2);
expect(wrapper.find(Link).at(0).text()).toContain('Forget');
expect(wrapper.find(Link).at(1).text()).toContain('Edit');
expect(wrapper.find('MenuItem').length).toBe(2);
expect(wrapper.find('MenuItem').at(0).text()).toBe('Rename');
expect(wrapper.find('MenuItem').at(1).text()).toBe('Forget Account');
});
});
@@ -7,7 +7,7 @@ import { AccountJson } from '@polkadot/extension/background/types';
import React, { useContext, useState } from 'react';
import styled from 'styled-components';
import { ActionBar, ActionContext, Address, Link } from '../../components';
import { ActionContext, Address, Link } from '../../components';
import { editAccount } from '../../messaging';
import { Name } from '../../partials';
@@ -33,37 +33,62 @@ function Account ({ address, className, isExternal }: Props): React.ReactElement
};
return (
<Address
address={address}
className={className}
name={editedName}
>
{isEditing && (
<Name
address={address}
className='edit-name'
isFocussed
label={null}
onBlur={_saveChanges}
onChange={setName}
/>
)}
<ActionBar>
<div>
<Link isDanger to={`/account/forget/${address}`}>Forget</Link>
{!isExternal && <Link to={`/account/export/${address}`}>Export</Link>}
</div>
<Link onClick={_toggleEdit}>Edit</Link>
</ActionBar>
</Address>
<div className={className}>
<Address
address={address}
name={editedName}
actions={(
<>
<MenuGroup>
<MenuItem onClick={_toggleEdit}>Rename</MenuItem>
</MenuGroup>
{!isExternal && <MenuItem isDanger to={`/account/export/${address}`}>Export Account</MenuItem>}
<MenuItem isDanger to={`/account/forget/${address}`}>Forget Account</MenuItem>
</>
)}
>
{isEditing && (
<Name
address={address}
className='edit-name'
isFocussed
label={' '}
onBlur={_saveChanges}
onChange={setName}
/>
)}
</Address>
</div>
);
}
const MenuGroup = styled.div`
padding-bottom: 16px;
margin-bottom: 16px;
border-bottom: 1px solid ${({ theme }): string => theme.boxBorderColor};
`;
const MenuItem = styled(Link)`
padding: 4px 16px;
display: block;
border-radius: 8px;
font-weight: 600;
font-size: 15px;
line-height: 20px;
`;
MenuItem.displayName = 'MenuItem';
export default styled(Account)`
${Address} {
margin-bottom: 8px;
}
.edit-name {
left: 3rem;
position: absolute;
right: 0.75rem;
top: -0.5rem;
flex: 1;
left: 80px;
top: 6px;
width: 315px;
}
`;
@@ -3,46 +3,88 @@
// of the Apache-2.0 license. See the LICENSE file for details.
import React, { useContext } from 'react';
import QrImage from '../../assets/qr.svg';
import { AccountContext, Button, Header, Link, MediaContext, Tip } from '../../components';
import {
AccountContext,
Button,
Header,
MediaContext,
AddAccount,
ButtonArea,
Svg,
ButtonWithSubtitle
} from '../../components';
import Account from './Account';
import styled from 'styled-components';
type Props = {};
export default function Accounts (): React.ReactElement<Props> {
const accounts = useContext(AccountContext);
const mediaAllowed = useContext(MediaContext);
return (
<div>
<Header
label='accounts'
labelExtra={<Link to='/settings'>Options</Link>}
/>
<>
{
(accounts.length === 0)
? <Tip header='add accounts' type='warn'>You currently don&apos;t have any accounts. Either create a new account or if you have an existing account you wish to use, import it with the seed phrase</Tip>
: accounts.map((json, index): React.ReactNode => (
<Account
{...json}
key={`${index}:${json.address}`}
/>
))
? <AddAccount />
: (
<>
<Header showSettings text={'Accounts'} />
<AccountsArea>
<>
{
accounts.map((json, index): React.ReactNode => (
<Account
{...json}
key={`${index}:${json.address}`}
/>
))
}
</>
</AccountsArea>
</>
)
}
<Button
label='I want to create a new account with a new seed'
to='/account/create'
/>
<Button
label='I have a pre-existing seed, import the account'
to='/account/import-seed'
/>
{mediaAllowed && (
<Button
label='I have an external account, add it via QR'
to='/account/import-qr'
<ButtonArea>
<ButtonWithSubtitle
to='/account/create'
title='Create New Account'
subTitle='With new seed'
/>
)}
</div>
<ButtonWithSubtitle
to='/account/import-seed'
title='Import an Account'
subTitle='I have a pre-existing seed'
/>
{mediaAllowed && (
<QrButton to='/account/import-qr'>
<Svg src={QrImage} />
</QrButton>
)}
</ButtonArea>
</>
);
}
const QrButton = styled(Button)`
width: 60px;
${Svg} {
width: 20px;
height: 20px;
background: ${({ theme }): string => theme.buttonTextColor};
}
`;
const AccountsArea = styled.div`
height: 100%;
overflow-y: scroll;
margin-top: -25px;
padding-top: 25px;
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
`;
@@ -0,0 +1,49 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React from 'react';
import Adapter from 'enzyme-adapter-react-16';
import { ThemeProvider } from 'styled-components';
import { AuthorizeContext, themes, Header, Icon } from '@polkadot/extension-ui/components';
import Authorize from '.';
import { configure, mount, ReactWrapper } from 'enzyme';
import { AuthorizeRequest } from '@polkadot/extension/background/types';
import Request from './Request';
configure({ adapter: new Adapter() });
describe('Authorize', () => {
const mountAuthorize = (authorizeRequests: AuthorizeRequest[] = []): ReactWrapper => mount(
<AuthorizeContext.Provider value={authorizeRequests}>
<ThemeProvider theme={themes.dark}>
<Authorize />
</ThemeProvider>
</AuthorizeContext.Provider>);
it('render component', () => {
const wrapper = mountAuthorize();
expect(wrapper.find(Header).text()).toBe('Authorize');
expect(wrapper.find(Request).length).toBe(0);
});
it('render requests', () => {
const wrapper = mountAuthorize([{ id: '1', request: { origin: '???' }, url: 'http://polkadot.org' }]);
expect(wrapper.find(Request).length).toBe(1);
expect(wrapper.find(Request).find('.tab-info').text()).toBe('An application, self-identifying as ??? is requesting access from http://polkadot.org.');
});
it('render more request but just one accept button', () => {
const wrapper = mountAuthorize([
{ id: '1', request: { origin: '???' }, url: 'http://polkadot.org' },
{ id: '2', request: { origin: 'abc' }, url: 'http://polkadot.pl' }
]);
expect(wrapper.find('AcceptButton').length).toBe(1);
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.');
});
});
@@ -7,7 +7,7 @@ import { RequestAuthorizeTab } from '@polkadot/extension/background/types';
import React, { useContext } from 'react';
import styled from 'styled-components';
import { ActionBar, ActionContext, Button, Icon, IconBox, Link, Tip, defaults } from '../../components';
import { ActionBar, ActionContext, Button, Icon, Link, Warning } from '../../components';
import { approveAuthRequest, rejectAuthRequest } from '../../messaging';
interface Props {
@@ -30,52 +30,93 @@ function Request ({ authId, className, isFirst, request: { origin }, url }: Prop
.catch((error: Error) => console.error(error));
return (
<IconBox
className={className}
icon={
<Icon
icon='X'
onClick={_onReject}
/>
}
intro={
<div className='tab-info'>An application, self-identifying as <span className='tab-name'>{origin}</span> is requesting access from <span className='tab-url'>{url}</span>.</div>
}
>
<ActionBar>
<div />
<Link isDanger onClick={_onReject}>Reject</Link>
</ActionBar>
{isFirst && (
<>
<Tip header='access' type='warn'>Only approve this request if you trust the application. Approving gives the application access to the addresses of your accounts.</Tip>
<Button
label='Yes, allow this application access'
onClick={_onApprove}
/>
</>
)}
</IconBox>
<div className={className}>
<RequestInfo>
<Info>
<Icon icon='X' onClick={_onReject} />
<div className='tab-info'>
An application, self-identifying as <span className='tab-name'>{origin}</span> is requesting access from{' '}
<a
href={url}
target="_blank"
rel="noopener noreferrer"
>
<span className='tab-url'>{url}</span>
</a>.
</div>
</Info>
{isFirst && (
<>
<RequestWarning>Only approve this request if you trust the application. Approving gives the application access to the addresses of your accounts.</RequestWarning>
<AcceptButton onClick={_onApprove}>Yes, allow this application access</AcceptButton>
</>
)}
<RejectButton>
<Link isDanger onClick={_onReject}>Reject</Link>
</RejectButton>
</RequestInfo>
</div>
);
}
const RequestInfo = styled.div`
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 8px;
background: ${({ theme }): 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)`
.icon {
background: ${defaults.btnBgDanger};
color: ${defaults.btnColorDanger};
background: ${({ theme }): string => theme.buttonBackgroundDanger};
color: white;
min-width: 18px;
width: 14px;
height: 18px;
font-size: 10px;
line-height: 20px;
margin: 16px 15px 0 1.35rem;
font-weight: 800;
padding-left: 0.5px;
}
.tab-info {
overflow: hidden;
margin: 0.75rem 20px 0 0;
}
.tab-name,
.tab-url {
color: ${defaults.linkColor};
color: ${({ theme }): string => theme.textColor};
display: inline-block;
max-width: 20rem;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: top;
cursor: pointer;
text-decoration: underline;
}
`;
@@ -6,22 +6,37 @@ import React, { useContext } from 'react';
import { AuthorizeContext, Header } from '../../components';
import Request from './Request';
import styled from 'styled-components';
export default function Authorize (): React.ReactElement<{}> {
const requests = useContext(AuthorizeContext);
return (
<div>
<Header label='authorize' />
{requests.map(({ id, request, url }, index): React.ReactNode => (
<Request
authId={id}
isFirst={index === 0}
key={id}
request={request}
url={url}
/>
))}
</div>
<>
<Scroll isLastRequest={requests.length === 1}>
<Header text='Authorize'/>
{requests.map(({ id, request, url }, index): React.ReactNode => (
<Request
authId={id}
isFirst={index === 0}
key={id}
request={request}
url={url}
/>
))}
</Scroll>
</>
);
}
const Scroll = styled.div<{isLastRequest: boolean}>`
overflow-y: ${({ isLastRequest }): string => isLastRequest ? 'hidden' : 'auto'};
&& {
padding: 0;
}
${Request} {
padding: 0 24px;
}
`;
@@ -1,66 +0,0 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React, { useContext, useEffect, useState } from 'react';
import { ActionContext, Address, Button, Header, Loading, TextArea } from '../components';
import { createAccountSuri, createSeed } from '../messaging';
import { Back, Name, Password } from '../partials';
type Props = {}
export default function Create (): React.ReactElement<Props> {
const onAction = useContext(ActionContext);
const [account, setAccount] = useState<null | { address: string; seed: string }>(null);
const [name, setName] = useState<string | null>(null);
const [password, setPassword] = useState<string | null>(null);
useEffect((): void => {
createSeed()
.then(setAccount)
.catch((error: Error) => console.error(error));
}, []);
// FIXME Duplicated between here and Import.tsx
const _onCreate = (): void => {
// this should always be the case
if (name && password && account) {
createAccountSuri(name, password, account.seed)
.then((): void => onAction('/'))
.catch((error: Error) => console.error(error));
}
};
return (
<div>
<Header label='create account' />
<Back />
<Loading>{account && (
<>
<TextArea
isReadOnly
label='generated 12-word mnemonic seed'
value={account.seed}
/>
<Name
isFocussed
onChange={setName}
/>
{name && <Password onChange={setPassword} />}
{name && password && (
<Address
address={account.address}
name={name}
>
<Button
label='Add the account with the generated seed'
onClick={_onCreate}
/>
</Address>
)}
</>
)}</Loading>
</div>
);
}
@@ -0,0 +1,45 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React, { useState } from 'react';
import { Address, Button, ButtonArea, VerticalSpace } from '@polkadot/extension-ui/components';
import { Name, Password } from '@polkadot/extension-ui/partials';
interface Props {
onCreate: (name: string, password: string) => void | Promise<void | boolean>;
address: string;
}
function AccountName ({ onCreate, address }: Props): React.ReactElement<Props> {
const [name, setName] = useState<string | null>(null);
const [password, setPassword] = useState<string | null>(null);
return (
<>
<Name
isFocussed
onChange={setName}
/>
{name && <Password onChange={setPassword} />}
{name && password && (
<>
<Address
address={address}
name={name}
/>
<VerticalSpace />
<ButtonArea>
<Button
onClick={(): void | Promise<void | boolean> => onCreate(name, password)}
>
Add the account with the generated seed
</Button>
</ButtonArea>
</>
)}
</>
);
}
export default AccountName;
@@ -0,0 +1,143 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React from 'react';
import Adapter from 'enzyme-adapter-react-16';
import CreateAccount from '.';
import { configure, mount, ReactWrapper } from 'enzyme';
import * as messaging from '@polkadot/extension-ui/messaging';
import { act } from 'react-dom/test-utils';
import { flushAllPromises } from '@polkadot/extension-ui/testHelpers';
import {
ActionContext,
ActionText,
Button,
themes,
Input,
InputWithLabel,
Header
} from '@polkadot/extension-ui/components';
import { ThemeProvider } from 'styled-components';
configure({ adapter: new Adapter() });
describe('Create Account', () => {
let wrapper: ReactWrapper;
let onActionStub: jest.Mock;
const exampleAccount = {
seed: 'horse battery staple correct',
address: 'HjoBp62cvsWDA3vtNMWxz6c9q13ReEHi9UGHK7JbZweH5g5'
};
const mountComponent = (): ReactWrapper => mount(
<ActionContext.Provider value={onActionStub}>
<ThemeProvider theme={themes.dark}>
<CreateAccount />
</ThemeProvider>
</ActionContext.Provider>
);
const check = (input: ReactWrapper): unknown => input.simulate('change', { target: { checked: true } });
beforeEach(async () => {
onActionStub = jest.fn();
jest.spyOn(messaging, 'createSeed').mockResolvedValue(exampleAccount);
jest.spyOn(messaging, 'createAccountSuri').mockResolvedValue(true);
wrapper = mountComponent();
await act(flushAllPromises);
wrapper.update();
});
describe('Phase 1', () => {
it('shows seed phrase in textarea', () => {
expect(wrapper.find('textarea').text()).toBe(exampleAccount.seed);
});
it('next step button is disabled when checkbox is not checked', () => {
expect(wrapper.find(Button).prop('isDisabled')).toBe(true);
});
it('action text is "Cancel"', () => {
expect(wrapper.find(Header).find(ActionText).text()).toBe('Cancel');
});
it('clicking "Cancel" redirects to main screen', () => {
wrapper.find(Header).find(ActionText).simulate('click');
expect(onActionStub).toBeCalledWith('/');
});
it('clicking on Next activates phase 2', () => {
check(wrapper.find('input[type="checkbox"]'));
wrapper.find('button').simulate('click');
expect(wrapper.find(Header).text()).toBe('Create an account 2/2Back');
});
});
describe('Phase 2', () => {
const type = (input: ReactWrapper, value: string): unknown => input.simulate('change', { target: { value } });
beforeEach(() => {
check(wrapper.find('input[type="checkbox"]'));
wrapper.find('button').simulate('click');
});
it('only account name input is visible at first', () => {
expect(wrapper.find(InputWithLabel).find('[data-input-name]').find(Input)).toHaveLength(1);
expect(wrapper.find(InputWithLabel).find('[data-input-password]')).toHaveLength(0);
expect(wrapper.find(InputWithLabel).find('[data-input-repeat-password]')).toHaveLength(0);
expect(wrapper.find(Button)).toHaveLength(0);
});
it('after typing less than 3 characters into name input, password input is not visible', () => {
type(wrapper.find('input'), 'ab');
expect(wrapper.find(Input).prop('withError')).toBe(true);
expect(wrapper.find(InputWithLabel).find('[data-input-password]')).toHaveLength(0);
expect(wrapper.find(InputWithLabel).find('[data-input-repeat-password]')).toHaveLength(0);
expect(wrapper.find(Button)).toHaveLength(0);
});
it('after typing 3 characters into name input, first password input is visible', () => {
type(wrapper.find('input'), 'abc');
expect(wrapper.find(Input).first().prop('withError')).toBe(false);
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(Button)).toHaveLength(0);
});
it('password shorter than 6 characters should be not valid', () => {
type(wrapper.find('input'), 'abc');
type(wrapper.find('input[type="password"]'), 'abcde');
expect(wrapper.find(InputWithLabel).find('[data-input-password]').find(Input).prop('withError')).toBe(true);
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(Button)).toHaveLength(0);
});
it('submit button is not visible until both passwords are equal', () => {
type(wrapper.find('input'), 'abc');
type(wrapper.find('input[type="password"]').first(), 'abcdef');
type(wrapper.find('input[type="password"]').last(), 'abcdeg');
expect(wrapper.find(InputWithLabel).find('[data-input-repeat-password]').find(Input).prop('withError')).toBe(true);
expect(wrapper.find(Button)).toHaveLength(0);
});
it('submit button is visible when both passwords are equal', () => {
type(wrapper.find('input'), 'abc');
type(wrapper.find('input[type="password"]').first(), 'abcdef');
type(wrapper.find('input[type="password"]').last(), 'abcdef');
expect(wrapper.find(InputWithLabel).find('[data-input-repeat-password]').find(Input).prop('withError')).toBe(false);
expect(wrapper.find(Button)).toHaveLength(1);
});
it('saves account with provided name and password', async () => {
type(wrapper.find('input'), 'abc');
type(wrapper.find('input[type="password"]').first(), 'abcdef');
type(wrapper.find('input[type="password"]').last(), 'abcdef');
wrapper.find(Button).find('button').simulate('click');
expect(messaging.createAccountSuri).toBeCalledWith('abc', 'abcdef', exampleAccount.seed);
await flushAllPromises();
expect(onActionStub).toBeCalledWith('/');
});
});
});
@@ -0,0 +1,54 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React from 'react';
import styled from 'styled-components';
import { ActionText, Title } from '@polkadot/extension-ui/components';
interface Props {
step: number;
onClick: () => void;
className?: string;
}
function CreationStep ({ step, onClick, className }: Props): React.ReactElement<Props> {
return (
<div className={className}>
<div>
<CreateAnAccount />
<CurrentStep>{step}</CurrentStep>
<TotalSteps>/2</TotalSteps>
</div>
<ActionText text={step === 1 ? 'Cancel' : 'Back'} onClick={onClick} />
</div>
);
}
const CreateAnAccount = styled(Title).attrs(() => ({
children: 'Create an account:'
}))`
display: inline;
margin-right: 10px;
`;
const CurrentStep = styled.span`
font-size: ${({ theme }): string => theme.labelFontSize};
line-height: ${({ theme }): string => theme.labelLineHeight};
color: ${({ theme }): string => theme.primaryColor};
font-weight: 600;
`;
const TotalSteps = styled.span`
font-size: ${({ theme }): string => theme.labelFontSize};
line-height: ${({ theme }): string => theme.labelLineHeight};
color: ${({ theme }): string => theme.textColor};
font-weight: 600;
`;
export default styled(CreationStep)`
display: flex;
flex-direction: row;
justify-content: space-between;
margin-bottom: 17px;
`;
@@ -0,0 +1,47 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React, { useState } from 'react';
import { Button, ButtonArea, Checkbox, MnemonicSeed, VerticalSpace, Warning } from '../../components';
interface Props {
seed: string;
onNextStep: () => void;
}
const onCopy = (): void => {
const mnemonicSeedTextElement = document.querySelector('textarea');
if (!mnemonicSeedTextElement) {
return;
}
mnemonicSeedTextElement.select();
document.execCommand('copy');
};
function Mnemonic ({ seed, onNextStep }: Props): React.ReactElement<Props> {
const [isMnemonicSaved, setIsMnemonicSaved] = useState(false);
return (
<>
<Warning>
Please write down your wallet’s mnemonic seed and keep it in a safe place. <br />
Mnemonic seed is used to restore your wallet. Keep it carefully in case you lose your assets.
</Warning>
<MnemonicSeed
seed={seed}
onCopy={onCopy}
/>
<VerticalSpace />
<Checkbox
checked={isMnemonicSaved}
onChange={setIsMnemonicSaved}
label='I have saved my mnemonic seed safely.'
/>
<ButtonArea>
<Button isDisabled={!isMnemonicSaved} onClick={onNextStep}>Next step</Button>
</ButtonArea>
</>
);
}
export default Mnemonic;
@@ -0,0 +1,85 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React, { useContext, useEffect, useState } from 'react';
import styled from 'styled-components';
import { ActionContext, Header, Loading, ActionText } from '../../components';
import { createAccountSuri, createSeed } from '../../messaging';
import Mnemonic from '@polkadot/extension-ui/Popup/CreateAccount/Mnemonic';
import AccountName from '@polkadot/extension-ui/Popup/CreateAccount/AccountName';
export default function CreateAccount (): React.ReactElement {
const onAction = useContext(ActionContext);
const [step, setStep] = useState(1);
const [account, setAccount] = useState<null | { address: string; seed: string }>(null);
useEffect((): void => {
createSeed()
.then(setAccount)
.catch((error: Error) => console.error(error));
}, []);
// FIXME Duplicated between here and Import.tsx
const _onCreate = (name: string, password: string): void => {
// this should always be the case
if (name && password && account) {
createAccountSuri(name, password, account.seed)
.then((): void => onAction('/'))
.catch((error: Error) => console.error(error));
}
};
const _onNextStep = (): void => setStep(step + 1);
const _onCancel = (): void => {
if (step === 2) {
setStep(step - 1);
} else {
onAction('/');
}
};
return (
<>
<Header text={'Create an account '}>
<CreationSteps>
<div>
<CurrentStep>{step}</CurrentStep>
<TotalSteps>/2</TotalSteps>
</div>
<ActionText text={step === 1 ? 'Cancel' : 'Back'} onClick={_onCancel} />
</CreationSteps>
</Header>
<Loading>{account && (step === 1 ? (
<Mnemonic seed={account.seed} onNextStep={_onNextStep} />
) : (
<AccountName address={account.address} onCreate={_onCreate} />
))}</Loading>
</>
);
}
const CreationSteps = styled.div`
align-items: center;
display: flex;
justify-content: space-between;
flex-grow: 1;
padding-right: 24px;
margin-top: 3px;
`;
const CurrentStep = styled.span`
font-size: ${({ theme }): string => theme.labelFontSize};
line-height: ${({ theme }): string => theme.labelLineHeight};
color: ${({ theme }): string => theme.primaryColor};
font-weight: 800;
margin-left: 10px;
`;
const TotalSteps = styled.span`
font-size: ${({ theme }): string => theme.labelFontSize};
line-height: ${({ theme }): string => theme.labelLineHeight};
color: ${({ theme }): string => theme.textColor};
font-weight: 800;
`;
@@ -7,15 +7,15 @@ import Adapter from 'enzyme-adapter-react-16';
import { configure, mount, ReactWrapper } from 'enzyme';
import { MemoryRouter, Route } from 'react-router';
import React from 'react';
import * as messaging from '@polkadot/extension-ui/messaging';
import { Button } from '../components';
import { Button, themes } from '../components';
import Export from './Export';
import { exportAccount } from '../messaging';
import { ThemeProvider } from 'styled-components';
import { flushAllPromises } from '@polkadot/extension-ui/testHelpers';
configure({ adapter: new Adapter() });
jest.mock('../messaging');
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
const flushAllPromises = (): Promise<void> => new Promise(resolve => setImmediate(resolve));
describe('Export component', () => {
let wrapper: ReactWrapper;
@@ -26,12 +26,15 @@ describe('Export component', () => {
};
beforeEach(() => {
(exportAccount as jest.Mock).mockResolvedValue({ exportedJson: '{ "meta": { "name": "account_name" } }' });
jest.spyOn(messaging, 'exportAccount').mockResolvedValue({ exportedJson: '{ "meta": { "name": "account_name" } }' });
wrapper = mount(<MemoryRouter
initialEntries={ [`/account/export/${VALID_ADDRESS}`] }>
<Route path='/account/export/:address' component={ Export }/>
</MemoryRouter>);
wrapper = mount(
<MemoryRouter initialEntries={ [`/account/export/${VALID_ADDRESS}`] }>
<ThemeProvider theme={themes.dark}>
<Route path='/account/export/:address' component={ Export } />
</ThemeProvider>
</MemoryRouter>
);
});
it('creates export message on button press', async () => {
+57 -24
View File
@@ -5,9 +5,18 @@
import React, { useContext, useState } from 'react';
import { RouteComponentProps, withRouter } from 'react-router';
import { ActionContext, Address, Button, Header, Input, Tip } from '../components';
import {
ActionContext,
Address,
Button,
InputWithLabel,
Warning,
Header,
ActionText,
ActionBar
} from '../components';
import { exportAccount } from '../messaging';
import { Back } from '../partials';
import styled from 'styled-components';
const MIN_LENGTH = 6;
@@ -35,29 +44,53 @@ function Export ({ match: { params: { address } } }: Props): React.ReactElement<
});
return (
<div>
<Header label='export account' />
<Back />
<Address address={address}>
<Tip header='danger' type='error'>You are exporting your account. Keep it safe and don&apos;t share it with anyone.</Tip>
<Input
isError={pass.length < MIN_LENGTH || wrongPasswordHighlight}
label='password for this account'
onChange={setPass}
type='password'
data-export-password
/>
<Button
isDisabled={pass.length === 0}
isDanger
label='I want to export this account'
onClick={_onExportButtonClick}
className='export-button'
data-export-button
/>
</Address>
</div>
<>
<Header text='Export account' showBackArrow/>
<div>
<Address address={address}>
<MovedWarning danger>You are exporting your account. Keep it safe and don&apos;t share it with anyone.</MovedWarning>
<ActionArea>
<InputWithLabel
isError={pass.length < MIN_LENGTH || wrongPasswordHighlight}
label='password for this account'
onChange={setPass}
type='password'
data-export-password
/>
<Button
isDisabled={pass.length === 0}
isDanger
onClick={_onExportButtonClick}
className='export-button'
data-export-button
>
I want to export this account
</Button>
<CancelButton>
<ActionText text='Cancel' onClick={(): void => onAction('/')} />
</CancelButton>
</ActionArea>
</Address>
</div>
</>
);
}
const MovedWarning = styled(Warning)`
margin-top: 8px;
`;
const ActionArea = styled.div`
padding: 10px 24px;
`;
const CancelButton = styled(ActionBar)`
margin-top: 4px;
text-decoration: underline;
${ActionText} {
margin: auto;
}
`;
export default withRouter(Export);
+39 -14
View File
@@ -4,10 +4,10 @@
import React, { useContext } from 'react';
import { RouteComponentProps, withRouter } from 'react-router';
import { ActionContext, Address, Button, Warning, Header, ActionBar, ActionText } from '../components';
import { ActionContext, Address, Button, Header, Tip } from '../components';
import { forgetAccount } from '../messaging';
import { Back } from '../partials';
import styled from 'styled-components';
type Props = RouteComponentProps<{ address: string }>;
@@ -20,19 +20,44 @@ function Forget ({ match: { params: { address } } }: Props): React.ReactElement<
.catch((error: Error) => console.error(error));
return (
<div>
<Header label='forget account' />
<Back />
<Address address={address}>
<Tip header='danger' type='error'>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.</Tip>
<Button
isDanger
label='I want to forget this account'
onClick={_onClick}
/>
</Address>
</div>
<>
<Header text='Forget account' showBackArrow/>
<div>
<Address address={address}>
<MovedWarning danger>
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>
<Button
isDanger
onClick={_onClick}
>
I want to forget this account
</Button>
<CancelButton>
<ActionText text='Cancel' onClick={(): void => onAction('/')} />
</CancelButton>
</ActionArea>
</Address>
</div>
</>
);
}
const MovedWarning = styled(Warning)`
margin-top: 8px;
`;
const ActionArea = styled.div`
padding: 10px 24px;
`;
const CancelButton = styled(ActionBar)`
margin-top: 4px;
${ActionText} {
margin: auto;
}
`;
export default withRouter(Forget);
+19 -15
View File
@@ -5,9 +5,9 @@
import React, { useContext, useState } from 'react';
import { QrScanAddress } from '@polkadot/react-qr';
import { ActionContext, Address, Button, Header } from '../components';
import { ActionContext, Address, Button, ButtonArea, Header, VerticalSpace } from '../components';
import { createAccountExternal } from '../messaging';
import { Back, Name } from '../partials';
import { Name } from '../partials';
type Props = {};
@@ -26,10 +26,13 @@ export default function ImportQr (): React.ReactElement<Props> {
};
return (
<div>
<Header label='import external' />
<Back />
{!account && <QrScanAddress onScan={setAccount} />}
<>
<Header showBackArrow text='Scan Address Qr' />
{!account && (
<div>
<QrScanAddress onScan={setAccount} />
</div>
)}
{account && (
<>
<Name
@@ -39,16 +42,17 @@ export default function ImportQr (): React.ReactElement<Props> {
<Address
{...account}
name={name}
>
{name && (
<Button
label='Add the account with identified address'
onClick={_onCreate}
/>
)}
</Address>
/>
<VerticalSpace />
{name && (
<ButtonArea>
<Button onClick={_onCreate}>
Add the account with identified address
</Button>
</ButtonArea>
)}
</>
)}
</div>
</>
);
}
+38 -16
View File
@@ -4,9 +4,18 @@
import React, { useContext, useState } from 'react';
import { ActionContext, Address, Button, Header, TextArea } from '../components';
import {
ActionContext,
Address,
Button,
ButtonArea,
Header,
TextAreaWithLabel,
VerticalSpace
} from '../components';
import { createAccountSuri, validateSeed } from '../messaging';
import { Back, Name, Password } from '../partials';
import { Name, Password } from '../partials';
import styled from 'styled-components';
type Props = {};
@@ -32,28 +41,41 @@ export default function Import (): React.ReactElement<Props> {
};
return (
<div>
<Header label='import account' />
<Back />
<TextArea
<>
<HeaderWithSmallerMargin text='Import account' showBackArrow />
<SeedInput
rowsCount={2}
isError={!account}
isFocussed
isFocused
label='existing 12 or 24-word mnemonic seed'
onChange={_onChangeSeed}
/>
{account && <Name onChange={setName} />}
{account && name && <Password onChange={setPassword} />}
{account && name && password && (
<Address
address={account.address}
name={name}
>
<Button
label='Add the account with the supplied seed'
onClick={_onCreate}
<>
<Address
address={account.address}
name={name}
/>
</Address>
<VerticalSpace />
<ButtonArea>
<Button onClick={_onCreate}>Add the account with the supplied seed</Button>
</ButtonArea>
</>
)}
</div>
</>
);
}
const HeaderWithSmallerMargin = styled(Header)`
margin-bottom: 15px;
`;
const SeedInput = styled(TextAreaWithLabel)`
margin-bottom: 16px;
textarea {
height: unset;
}
`;
@@ -6,7 +6,7 @@ import React, { useState } from 'react';
import settings from '@polkadot/ui-settings';
import { setSS58Format } from '@polkadot/util-crypto';
import { Button, Dropdown, Header } from '../../components';
import { Button, Dropdown } from '../../components';
import { windowOpen } from '../../messaging';
import { Back } from '../../partials';
@@ -47,7 +47,6 @@ export default function Settings (): React.ReactElement<{}> {
return (
<div>
<Header label='settings' />
<Back />
<Dropdown
label='display addresses formatted for'
@@ -62,10 +61,7 @@ export default function Settings (): React.ReactElement<{}> {
value={camera}
/>
{isPopup && (
<Button
label='Open extension in new window'
onClick={windowOpen}
/>
<Button onClick={windowOpen}>Open extension in new window</Button>
)}
</div>
);
@@ -99,7 +99,7 @@ function Extrinsic ({ className, isDecoded, payload: { era, nonce, tip }, reques
useEffect((): void => {
setDecoded(decodeMethod(method, isDecoded, chain, specVersion));
}, [isDecoded]);
}, [method, isDecoded, chain, specVersion]);
return (
<table className={className}>
@@ -137,10 +137,12 @@ function Extrinsic ({ className, isDecoded, payload: { era, nonce, tip }, reques
}
export default styled(Extrinsic)`
border: 0;
height: 100%;
overflow: scroll;
display: block;
font-size: 0.75rem;
margin-top: 0.75rem;
border: 0;
font-size: ${({ theme }): string => theme.labelFontSize};
line-height: ${({ theme }): string => theme.labelLineHeight};
td.data {
max-width: 0;
+28 -14
View File
@@ -31,24 +31,38 @@ function Qr ({ className, onSignature, payload, request }: Props): React.ReactEl
return (
<div className={className}>
{isScanning
? <QrScanSignature onScan={onSignature} />
: <QrDisplayPayload
address={request.address}
cmd={CMD_MORTAL}
payload={payloadU8a}
/>
}
<QrArea>
{isScanning
? <QrScanSignature onScan={onSignature} />
: (
<QrDisplayPayload
address={request.address}
cmd={CMD_MORTAL}
payload={payloadU8a}
/>
)
}
</QrArea>
{!isScanning && (
<Button
label='Scan signature via camera'
onClick={_onShowQr}
/>
<ScanButton onClick={_onShowQr}>Scan signature via camera</ScanButton>
)}
</div>
);
}
export default styled(Qr)`
padding: 0.75rem 1.5rem 0 1.5rem;
const ScanButton = styled(Button)`
margin-bottom: 8px;
`;
const QrArea = styled.div`
width: 65%;
margin: 5px auto 10px auto;
img {
border: white solid 1px;
}
`;
export default styled(Qr)`
height: 100%;
`;
@@ -9,25 +9,27 @@ import { SignerPayloadJSON, SignerPayloadRaw } from '@polkadot/types/types';
import React, { useContext, useState, useEffect } from 'react';
import { TypeRegistry, createType } from '@polkadot/types';
import { ActionBar, ActionContext, Address, Link } from '../../components';
import { ActionBar, ActionContext, Address, ButtonArea, Link, VerticalSpace } from '../../components';
import { approveSignPassword, approveSignSignature, cancelSignRequest } 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;
isFirst: boolean;
request: RequestSign;
signId: string;
url: string;
isFirst?: boolean;
buttonText?: string;
}
// keep it global, we can and will re-use this across requests
const registry = new TypeRegistry();
export default function Request ({ account: { isExternal }, isFirst, request, signId, url }: Props): React.ReactElement<Props> | null {
export default function Request ({ account: { isExternal }, request, signId, url, isFirst, buttonText }: Props): React.ReactElement<Props> | null {
const onAction = useContext(ActionContext);
const [hexBytes, setHexBytes] = useState<string | null>(null);
const [extrinsic, setExtrinsic] = useState<ExtrinsicPayload | null>(null);
@@ -36,15 +38,17 @@ export default function Request ({ account: { isExternal }, isFirst, request, si
const inner = request.inner;
if ((inner as SignerPayloadRaw).data) {
setHexBytes((inner as SignerPayloadRaw).data);
setExtrinsic(null);
} else {
setExtrinsic(createType(registry, 'ExtrinsicPayload', inner, { version: (inner as SignerPayloadJSON).version }));
setHexBytes(null);
}
}, [request]);
const _onCancel = (): Promise<void> =>
cancelSignRequest(signId)
.then((): void => onAction())
const _onCancel = (): Promise<void> => {
return cancelSignRequest(signId).then((): void => onAction())
.catch((error: Error) => console.error(error));
};
const _onSign = (password: string): Promise<void> =>
approveSignPassword(signId, password)
.then((): void => onAction())
@@ -53,46 +57,71 @@ export default function Request ({ account: { isExternal }, isFirst, request, si
approveSignSignature(signId, signature)
.then((): void => onAction())
.catch((error: Error) => console.error(error));
if (extrinsic !== null) {
const payload = request.inner as SignerPayloadJSON;
return (
<Address
address={payload.address}
genesisHash={payload.genesisHash}
>
{isExternal && isFirst
? <Qr
payload={extrinsic}
request={payload}
onSignature={_onSignature}
/>
: <Extrinsic
isDecoded={isFirst}
payload={extrinsic}
request={payload}
url={url}
/>
<>
<Address
address={payload.address}
genesisHash={payload.genesisHash}
/>
{isExternal
? (
<Qr
payload={extrinsic}
request={payload}
onSignature={_onSignature}
/>
) : (
<Extrinsic
isDecoded={true}
payload={extrinsic}
request={payload}
url={url}
/>
)
}
{isFirst && !isExternal && <Unlock onSign={_onSign} />}
<ActionBar>
<div />
<Link isDanger onClick={_onCancel}>Cancel</Link>
</ActionBar>
</Address>
<SignArea>
{isFirst && !isExternal && <Unlock onSign={_onSign} buttonText={buttonText} />}
<CancelButton>
<Link isDanger onClick={_onCancel}>Cancel</Link>
</CancelButton>
</SignArea>
</>
);
} else if (hexBytes !== null) {
const payload = request.inner as SignerPayloadRaw;
return (
<Address address={payload.address}>
<>
<Address address={payload.address} />
<Bytes bytes={payload.data} url={url} />
{isFirst && !isExternal && <Unlock onSign={_onSign} />}
<ActionBar>
<div />
<Link isDanger onClick={_onCancel}>Cancel</Link>
</ActionBar>
</Address>
<VerticalSpace />
<SignArea>
{!isExternal && <Unlock onSign={_onSign} buttonText={buttonText} />}
<CancelButton>
<Link isDanger onClick={_onCancel}>Reject</Link>
</CancelButton>
</SignArea>
</>
);
} else {
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';
@@ -0,0 +1,242 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React, { useState } from 'react';
import Adapter from 'enzyme-adapter-react-16';
import { configure, mount, ReactWrapper } from 'enzyme';
import { act } from 'react-dom/test-utils';
import * as messaging from '@polkadot/extension-ui/messaging';
import { AccountJson, SigningRequest } from '@polkadot/extension/background/types';
import { flushAllPromises } from '@polkadot/extension-ui/testHelpers';
import {
AccountContext,
ActionContext,
themes,
SigningContext,
Address,
Input,
Button
} from '@polkadot/extension-ui/components';
import { ThemeProvider } from 'styled-components';
import Signing from '.';
import TransactionIndex from './TransactionIndex';
import Request from './Request';
import Extrinsic from './Extrinsic';
import Qr from './Qr';
import { EventEmitter } from 'events';
configure({ adapter: new Adapter() });
describe('Signing requests', () => {
let wrapper: ReactWrapper;
let onActionStub: jest.Mock;
let signRequests: SigningRequest[] = [];
const accounts: AccountJson[] = [{
address: '5D4bqjQRPgdMBK8bNvhX4tSuCtSGZS7rZjD5XH5SoKcFeKn5',
genesisHash: '0xdcd1346701ca8396496e52aa2785b1748deb6db09551b72159dcb3e08991025b',
isExternal: false,
name: 'acc1'
}, {
address: '5E9nq1yGJJFiP8C75ryD9J2R62q2cesz6NumLnuXRgmuN5DG',
genesisHash: '0xdcd1346701ca8396496e52aa2785b1748deb6db09551b72159dcb3e08991025b',
isExternal: false,
name: 'acc2'
}];
const emitter = new EventEmitter();
function MockRequestsProvider (): React.ReactElement {
const [requests, setRequests] = useState(signRequests);
emitter.on('request', setRequests);
return (
<SigningContext.Provider value={requests}>
<Signing />
</SigningContext.Provider>
);
}
const mountComponent = async (): Promise<void> => {
wrapper = mount(
<ActionContext.Provider value={onActionStub}>
<AccountContext.Provider value={accounts}>
<ThemeProvider theme={themes.dark}>
<MockRequestsProvider />
</ThemeProvider>
</AccountContext.Provider>
</ActionContext.Provider>
);
await act(flushAllPromises);
wrapper.update();
};
beforeEach(async () => {
jest.spyOn(messaging, 'cancelSignRequest').mockResolvedValue(true);
jest.spyOn(messaging, 'approveSignPassword').mockResolvedValue(true);
signRequests = [{ // 0.031415926500000 DOT -> 5D4bqjQRPgdMBK8bNvhX4tSuCtSGZS7rZjD5XH5SoKcFeKn5
account: {
address: '5D4bqjQRPgdMBK8bNvhX4tSuCtSGZS7rZjD5XH5SoKcFeKn5',
name: 'acc1'
},
url: 'polkadot.js',
id: '1574174715509.78',
request: {
sign: jest.fn(),
inner: {
address: '5D4bqjQRPgdMBK8bNvhX4tSuCtSGZS7rZjD5XH5SoKcFeKn5',
blockHash: '0xc288fbc472dab27d13ce58212eeb1243f460c5b0f9a65e9de97cbbf9bc761cb0',
blockNumber: '0x00000000003d8c4a',
era: '0xa500',
genesisHash: '0xdcd1346701ca8396496e52aa2785b1748deb6db09551b72159dcb3e08991025b',
method: '0x0300ff2c27fb3518d84bfad60f39b2cb5502947746ca5921fd66dccc861bad5c9a65220ba0aa2397921c',
nonce: '0x0000000000000000',
specVersion: '0x00000070',
tip: '0x00000000000000000000000000000000',
version: 1
}
}
}, { // 10000000000 nDOT -> 5D1ss3KFnzNtLzRDfUhqLivzVvt5BDrBnK21dMf1si2twPuj
account: {
address: '5E9nq1yGJJFiP8C75ryD9J2R62q2cesz6NumLnuXRgmuN5DG',
name: 'acc2'
},
url: 'polkadot.js',
id: '1574174306604.76',
request: {
sign: jest.fn(),
inner: {
address: '5E9nq1yGJJFiP8C75ryD9J2R62q2cesz6NumLnuXRgmuN5DG',
blockHash: '0xf3b92cf71c84762ba1cb59dc4fd192f1824171a96b43bce44ceb0671b378d15a',
blockNumber: '0x00000000003d8e9d',
era: '0xd501',
genesisHash: '0xdcd1346701ca8396496e52aa2785b1748deb6db09551b72159dcb3e08991025b',
method: '0x0300ff2a142e8c67a1ddcf6241f4fabf55a0bb0ee41d8a681ab3b3662a75037025967c0700e40b5402',
nonce: '0x0000000000000000',
specVersion: '0x00000070',
tip: '0x00000000000000000000000000000000',
version: 1
}
}
}];
onActionStub = jest.fn();
await mountComponent();
});
describe('Switching between requests', () => {
it('initially first request should be shown', () => {
expect(wrapper.find(TransactionIndex).text()).toBe('1/2');
expect(wrapper.find(Request).prop('signId')).toBe(signRequests[0].id);
});
it('only ArrowRight should be active on first screen', () => {
expect(wrapper.find('ArrowLeft').prop('isActive')).toBe(false);
expect(wrapper.find('ArrowRight').prop('isActive')).toBe(true);
wrapper.find('ArrowLeft').simulate('click');
expect(wrapper.find(TransactionIndex).text()).toBe('1/2');
});
it('should display second request after clicking right arrow', () => {
wrapper.find('ArrowRight').simulate('click');
expect(wrapper.find(TransactionIndex).text()).toBe('2/2');
expect(wrapper.find(Request).prop('signId')).toBe(signRequests[1].id);
});
it('only ArrowLeft should be active on second screen', () => {
wrapper.find('ArrowRight').simulate('click');
expect(wrapper.find('ArrowLeft').prop('isActive')).toBe(true);
expect(wrapper.find('ArrowRight').prop('isActive')).toBe(false);
expect(wrapper.find(TransactionIndex).text()).toBe('2/2');
});
it('should display previous request after ArrowLeft has been clicked', () => {
wrapper.find('ArrowRight').simulate('click');
wrapper.find('ArrowLeft').simulate('click');
expect(wrapper.find(TransactionIndex).text()).toBe('1/2');
expect(wrapper.find(Request).prop('signId')).toBe(signRequests[0].id);
});
});
describe('External account', () => {
it('shows Qr scanner for external accounts', async () => {
signRequests = [{
account: {
address: '5Cf1CGZas62RWwce3d2EPqUvSoi1txaXKd9M5w9bEFSsQtRe',
isExternal: true,
name: 'external'
},
url: 'polkadot.js',
id: '1574174306604.76',
request: {
sign: jest.fn(),
inner: {
address: '5Cf1CGZas62RWwce3d2EPqUvSoi1txaXKd9M5w9bEFSsQtRe',
blockHash: '0xf3b92cf71c84762ba1cb59dc4fd192f1824171a96b43bce44ceb0671b378d15a',
blockNumber: '0x00000000003d8e9d',
era: '0xd501',
genesisHash: '0xdcd1346701ca8396496e52aa2785b1748deb6db09551b72159dcb3e08991025b',
method: '0x0300ff2a142e8c67a1ddcf6241f4fabf55a0bb0ee41d8a681ab3b3662a75037025967c0700e40b5402',
nonce: '0x0000000000000000',
specVersion: '0x00000070',
tip: '0x00000000000000000000000000000000',
version: 1
}
}
}];
await mountComponent();
expect(wrapper.find(Extrinsic)).toHaveLength(0);
expect(wrapper.find(Qr)).toHaveLength(1);
});
});
describe('Request rendering', () => {
it('correctly displays request 1', () => {
expect(wrapper.find(Address).find('FullAddress').text()).toBe('5D4bqjQRPgdMBK8bNvhX4tSuCtSGZS7rZjD5XH5SoKcFeKn5');
expect(wrapper.find(Extrinsic).find('td.data').map((el): string => el.text())).toEqual([
'polkadot.js',
'Alexander',
'112',
'0',
'balances.transfer',
`{
"dest": "5D4bqjQRPgdMBK8bNvhX4tSuCtSGZS7rZjD5XH5SoKcFeKn5",
"value": 31415926500000
}`,
' Transfer some liquid free balance to another account. `transfer` will set the `FreeBalance` of the sender and receiver. It will decrease the total issuance of the system by the `TransferFee`. If the sender\'s account is below the existential deposit as a result of the transfer, the account will be reaped. The dispatch origin for this call must be `Signed` by the transactor.',
'mortal, valid from #4,033,610 to #4,033,674'
]);
});
it('correctly displays request 2', () => {
wrapper.find('ArrowRight').simulate('click');
expect(wrapper.find(Address).find('FullAddress').text()).toBe('5E9nq1yGJJFiP8C75ryD9J2R62q2cesz6NumLnuXRgmuN5DG');
expect(wrapper.find(Extrinsic).find('td.data').at(5).text()).toBe(`{
"dest": "5D1ss3KFnzNtLzRDfUhqLivzVvt5BDrBnK21dMf1si2twPuj",
"value": 10000000000
}`);
});
});
describe('Submitting', () => {
it('passes request id to cancel call', () => {
wrapper.find('CancelButton').find('a').simulate('click');
expect(messaging.cancelSignRequest).toBeCalledWith(signRequests[0].id);
});
it('passes request id and password to approve call', () => {
wrapper.find(Input).simulate('change', { target: { value: 'hunter1' } });
wrapper.find(Button).find('button').simulate('click');
expect(messaging.approveSignPassword).toBeCalledWith(signRequests[0].id, 'hunter1');
});
it('when last request has been removed/cancelled, shows the previous one', () => {
wrapper.find('ArrowRight').simulate('click');
act(() => {
emitter.emit('request', [signRequests[0]]);
});
wrapper.update();
expect(wrapper.find(TransactionIndex)).toHaveLength(0);
expect(wrapper.find(Request).prop('signId')).toBe(signRequests[0].id);
});
});
});
@@ -0,0 +1,80 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React from 'react';
import styled from 'styled-components';
import { Svg } from '../../components';
import ArrowLeftImage from '../../assets/arrowLeft.svg';
interface Props {
index: number;
totalItems: number;
onNextClick: () => void;
onPreviousClick: () => void;
className?: string;
}
function TransactionIndex ({ index, totalItems, onNextClick, onPreviousClick }: Props): React.ReactElement<Props> {
const previousClickActive = index !== 0;
const nextClickActive = index < totalItems - 1;
return (
<CreationSteps>
<div>
<CurrentStep>{index + 1}</CurrentStep>
<TotalSteps>/{totalItems}</TotalSteps>
</div>
<div>
<ArrowLeft onClick={(): unknown => previousClickActive && onPreviousClick()} isActive={previousClickActive} />
<ArrowRight onClick={(): unknown => nextClickActive && onNextClick()} isActive={nextClickActive} />
</div>
</CreationSteps>
);
}
const CreationSteps = styled.div`
align-items: center;
display: flex;
justify-content: space-between;
flex-grow: 1;
padding-right: 24px;
`;
const CurrentStep = styled.span`
font-size: ${({ theme }): string => theme.labelFontSize};
line-height: ${({ theme }): string => theme.labelLineHeight};
color: ${({ theme }): string => theme.primaryColor};
font-weight: 600;
margin-left: 10px;
`;
const TotalSteps = styled.span`
font-size: ${({ theme }): string => theme.labelFontSize};
line-height: ${({ theme }): string => theme.labelLineHeight};
color: ${({ theme }): string => theme.textColor};
font-weight: 600;
`;
interface ArrowProps {
isActive: boolean;
}
const ArrowLeft = styled(Svg).attrs(() => ({
src: ArrowLeftImage
}))<ArrowProps>`
display: inline-block;
background: ${({ isActive, theme }): string => isActive ? theme.primaryColor : theme.iconNeutralColor};
cursor: ${({ isActive }): string => isActive ? 'pointer' : 'default'};
width: 12px;
height: 12px;
`;
ArrowLeft.displayName = 'ArrowLeft';
const ArrowRight = styled(ArrowLeft)`
margin-left: 6px;
transform: rotate(180deg);
`;
ArrowRight.displayName = 'ArrowRight';
export default TransactionIndex;
@@ -4,14 +4,15 @@
import React, { useState, useEffect } from 'react';
import { Button, Input } from '../../components';
import { Button, InputWithLabel } from '../../components';
interface Props {
className?: string;
onSign: (password: string) => Promise<void>;
buttonText?: string;
}
export default function Unlock ({ className, onSign }: Props): React.ReactElement<Props> {
export default function Unlock ({ className, onSign, buttonText = 'Sign the transaction' }: Props): React.ReactElement<Props> {
const [error, setError] = useState('');
const [password, setPassword] = useState('');
@@ -27,17 +28,14 @@ export default function Unlock ({ className, onSign }: Props): React.ReactElemen
return (
<div className={className}>
<Input
<InputWithLabel
isError={!password || !!error}
isFocussed
label='password for this account'
isFocused
label='Password for this account'
onChange={setPassword}
type='password'
/>
<Button
label='Sign the transaction'
onClick={_onClick}
/>
<Button onClick={_onClick}>{buttonText}</Button>
</div>
);
}
@@ -2,27 +2,47 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React, { useContext } from 'react';
import React, { useContext, useState, useEffect } from 'react';
import { Header, SigningContext } from '../../components';
import { Header, Loading, SigningContext } from '../../components';
import Request from './Request';
import TransactionIndex from './TransactionIndex';
import { SigningRequest } from '@polkadot/extension/background/types';
export default function Signing (): React.ReactElement<{}> {
const requests = useContext(SigningContext);
const [requestIndex, setRequestIndex] = useState(0);
return (
<div>
<Header label='transactions' />
{requests.map(({ account, id, request, url }, index): React.ReactNode => (
<Request
account={account}
isFirst={index === 0}
key={id}
request={request}
signId={id}
url={url}
/>
))}
</div>
);
useEffect(() => {
if (requestIndex >= requests.length) {
setRequestIndex(requests.length - 1);
}
}, [requests]);
function isTransaction (signingRequest: SigningRequest): boolean {
return 'blockNumber' in signingRequest.request.inner;
}
return requests[requestIndex] ? (
<>
<Header text={isTransaction(requests[requestIndex]) ? 'Transaction' : 'Sign message'}>
{requests.length > 1 && (
<TransactionIndex
index={requestIndex}
totalItems={requests.length}
onNextClick={(): void => setRequestIndex(requestIndex + 1)}
onPreviousClick={(): void => setRequestIndex(requestIndex - 1)}
/>
)}
</Header>
<Request
isFirst={requestIndex === 0}
account={requests[requestIndex].account}
request={requests[requestIndex].request}
signId={requests[requestIndex].id}
url={requests[requestIndex].url}
buttonText={isTransaction(requests[requestIndex]) ? 'Sign the transaction' : 'Sign the message'}
/>
</>
) : <Loading />;
}
+27 -14
View File
@@ -4,7 +4,8 @@
import React, { useContext } from 'react';
import { ActionContext, Box, Button, Header } from '../components';
import { ActionContext, Box, Button, ButtonArea, Header, List, VerticalSpace } from '../components';
import styled from 'styled-components';
type Props = {};
@@ -17,21 +18,33 @@ export default function Welcome (): React.ReactElement<Props> {
};
return (
<div>
<Header label='welcome' />
<Box>
Before we start, just a couple of notes regarding use -
<ul>
<>
<Header text='Welcome'/>
<Note>Before we start, just a couple of notes regarding use:</Note>
<TextBox>
<List>
<li>We do not send any clicks, pageviews or events to a central server</li>
<li>We do not use any trackers or analytics</li>
<li>We don&apos;t collect keys, addresses or any information - your information never leaves this machine</li>
</ul>
... we are not in the information collection business (even anonymized).
<Button
label='Understood, let me continue'
onClick={_onClick}
/>
</Box>
</div>
</List>
</TextBox>
<Note>... we are not in the information collection business (even anonymized).</Note>
<VerticalSpace />
<ButtonArea>
<Button onClick={_onClick}>Understood, let me continue</Button>
</ButtonArea>
</>
);
}
const Note = styled.p`
margin-bottom: 6px;
margin-top: 0;
color: ${({ theme }): string => theme.subTextColor};
`;
const TextBox = styled(Box)`
margin: 0.75rem 24px;
border: 1px solid ${({ theme }): string => theme.inputBorderColor};
color: ${({ theme }): string => theme.subTextColor};
`;
+2 -2
View File
@@ -14,7 +14,7 @@ import { AccountContext, ActionContext, AuthorizeContext, MediaContext, SigningC
import { subscribeAccounts, subscribeAuthorize, subscribeSigning } from '../messaging';
import Accounts from './Accounts';
import Authorize from './Authorize';
import Create from './Create';
import CreateAccount from './CreateAccount';
import Export from './Export';
import Forget from './Forget';
import ImportQr from './ImportQr';
@@ -95,7 +95,7 @@ export default function Popup (): React.ReactElement<{}> {
<MediaContext.Provider value={cameraOn && mediaAllowed}>
<SigningContext.Provider value={signRequests}>
<Switch>
<Route path='/account/create' component={Create} />
<Route path='/account/create' component={CreateAccount} />
<Route path='/account/forget/:address' component={Forget} />
<Route path='/account/export/:address' component={Export} />
<Route path='/account/import-qr' component={ImportQr} />
@@ -0,0 +1,3 @@
<svg width="8" height="6" viewBox="0 0 8 6" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8 0L3.97 6L0 0H8Z" fill="#585858"/>
</svg>

After

Width:  |  Height:  |  Size: 145 B

@@ -0,0 +1,4 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M13 7L1 7" stroke="#6C6C6C" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 13L1 7L7 1" stroke="#6C6C6C" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 317 B

@@ -0,0 +1,3 @@
<svg width="13" height="10" viewBox="0 0 13 10" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.9902 2.00008L11.5802 0.580078L4.99023 7.17008L2.41023 4.60008L0.990234 6.01008L4.99023 10.0001L12.9902 2.00008Z" fill="#FF8B00"/>
</svg>

After

Width:  |  Height:  |  Size: 248 B

@@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.7368 12.7273H3.78947V3.81818H10.7368V12.7273ZM10.7368 2.54545H3.78947C3.09474 2.54545 2.52632 3.11818 2.52632 3.81818V12.7273C2.52632 13.4273 3.09474 14 3.78947 14H10.7368C11.4316 14 12 13.4273 12 12.7273V3.81818C12 3.11818 11.4316 2.54545 10.7368 2.54545ZM8.8421 0H1.26316C0.568421 0 0 0.572727 0 1.27273V10.1818H1.26316V1.27273H8.8421V0Z" fill="#8E8E8E"/>
</svg>

After

Width:  |  Height:  |  Size: 476 B

@@ -0,0 +1,5 @@
<svg width="3" height="19" viewBox="0 0 3 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="1.5" cy="1.5" r="1.5" fill="#8E8E8E"/>
<circle cx="1.5" cy="9.5" r="1.5" fill="#8E8E8E"/>
<circle cx="1.5" cy="17.5" r="1.5" fill="#8E8E8E"/>
</svg>

After

Width:  |  Height:  |  Size: 261 B

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7 14H5V19H10V17H7V14ZM5 10H7V7H10V5H5V10ZM17 17H14V19H19V14H17V17ZM14 5V7H17V10H19V5H14Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 258 B

@@ -0,0 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.031 9.82502L15.0227 9.84169C15.056 9.56669 15.0893 9.28335 15.0893 9.00002C15.0893 8.71669 15.0643 8.45002 15.031 8.17502L15.0393 8.19169L17.0727 6.59169L15.0477 3.07502L12.656 4.04169L12.6643 4.05002C12.231 3.71669 11.756 3.43335 11.2393 3.21669H11.2477L10.8643 0.666687H6.80599L6.43932 3.22502H6.44766C5.93099 3.44169 5.45599 3.72502 5.02266 4.05835L5.03099 4.05002L2.63099 3.07502L0.597656 6.59169L2.63099 8.19169L2.63932 8.17502C2.60599 8.45002 2.58099 8.71669 2.58099 9.00002C2.58099 9.28335 2.60599 9.56669 2.64766 9.84169L2.63932 9.82502L0.889323 11.2L0.614323 11.4167L2.63932 14.9167L5.03932 13.9584L5.02266 13.925C5.46432 14.2667 5.93932 14.55 6.46432 14.7667H6.43932L6.81432 17.3334H10.856C10.856 17.3334 10.881 17.1834 10.906 16.9834L11.2227 14.775H11.2143C11.731 14.5584 12.2143 14.275 12.656 13.9334L12.6393 13.9667L15.0393 14.925L17.0643 11.425C17.0643 11.425 16.9477 11.325 16.7893 11.2084L15.031 9.82502ZM8.83099 11.9167C7.22266 11.9167 5.91432 10.6084 5.91432 9.00002C5.91432 7.39169 7.22266 6.08335 8.83099 6.08335C10.4393 6.08335 11.7477 7.39169 11.7477 9.00002C11.7477 10.6084 10.4393 11.9167 8.83099 11.9167Z" fill="#6C6C6C"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+23
View File
@@ -0,0 +1,23 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
declare module '*.svg' {
const url: string;
export default url;
}
declare module '*.png' {
const url: string;
export default url;
}
declare module '*.woff' {
const url: string;
export default url;
}
declare module '*.woff2' {
const url: string;
export default url;
}
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?><!-- Generator: Adobe Illustrator 23.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) --><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="15 15 140 140" style="enable-background:new 0 0 170 170;zoom: 1;" xml:space="preserve"><style type="text/css">
.bg0{fill:#FF8C00}
.st0{fill:#FFFFFF;}
</style><g><circle class="bg0" cx="85" cy="85" r="70"></circle><g><path class="st0" d="M85,34.7c-20.8,0-37.8,16.9-37.8,37.8c0,4.2,0.7,8.3,2,12.3c0.9,2.7,3.9,4.2,6.7,3.3c2.7-0.9,4.2-3.9,3.3-6.7 c-1.1-3.1-1.6-6.4-1.5-9.7C58.1,57.6,69.5,46,83.6,45.3c15.7-0.8,28.7,11.7,28.7,27.2c0,14.5-11.4,26.4-25.7,27.2 c0,0-5.3,0.3-7.9,0.7c-1.3,0.2-2.3,0.4-3,0.5c-0.3,0.1-0.6-0.2-0.5-0.5l0.9-4.4L81,73.4c0.6-2.8-1.2-5.6-4-6.2 c-2.8-0.6-5.6,1.2-6.2,4c0,0-11.8,55-11.9,55.6c-0.6,2.8,1.2,5.6,4,6.2c2.8,0.6,5.6-1.2,6.2-4c0.1-0.6,1.7-7.9,1.7-7.9 c1.2-5.6,5.8-9.7,11.2-10.4c1.2-0.2,5.9-0.5,5.9-0.5c19.5-1.5,34.9-17.8,34.9-37.7C122.8,51.6,105.8,34.7,85,34.7z M87.7,121.7 c-3.4-0.7-6.8,1.4-7.5,4.9c-0.7,3.4,1.4,6.8,4.9,7.5c3.4,0.7,6.8-1.4,7.5-4.9C93.3,125.7,91.2,122.4,87.7,121.7z"></path></g></g></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0V8H2V10H4V8H8V0H0ZM8 8V10H10V12H6V14H0V22H8V14H14V12H12V10H16V8H18V10H20V8H22V0H14V8H8ZM20 10V12H22V10H20ZM20 12H18V14H20V12ZM20 14V16H22V14H20ZM20 16H18V14H16V16H11V22H13V18H17V20H19V18H20V16ZM17 20H15V22H17V20ZM18 12V10H16V12H18ZM6 12V10H4V12H6ZM2 10H0V12H2V10ZM10 0V4H9V6H10V7H12V4H13V2H12V0H10ZM2 2H6V6H2V2ZM16 2H20V6H16V2ZM3 3V5H5V3H3ZM17 3V5H19V3H17ZM2 16H6V20H2V16ZM3 17V19H5V17H3ZM20 20V22H22V20H20Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 542 B

@@ -0,0 +1,3 @@
<svg width="16" height="14" viewBox="0 0 16 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.04729 0.808984L0.821115 12.1715C0.427365 12.8816 0.951193 13.75 1.77385 13.75H14.2297C15.0489 13.75 15.5727 12.8816 15.1824 12.1715L8.95276 0.808984C8.54143 0.0636719 7.45861 0.0636719 7.04729 0.808984ZM8.61877 5.52344L8.49221 9.8125H7.50783L7.38127 5.52344H8.61877ZM8.00002 12.1469C7.62385 12.1469 7.32854 11.8621 7.32854 11.5C7.32854 11.1379 7.62385 10.8531 8.00002 10.8531C8.37619 10.8531 8.67151 11.1379 8.67151 11.5C8.67151 11.8621 8.37619 12.1469 8.00002 12.1469Z" fill="#FF5858"/>
</svg>

After

Width:  |  Height:  |  Size: 605 B

@@ -0,0 +1,44 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React, { MouseEventHandler } from 'react';
import Svg from '@polkadot/extension-ui/components/Svg';
import styled from 'styled-components';
interface Props {
icon?: string;
text: string;
onClick: MouseEventHandler<HTMLDivElement>;
className?: string;
}
function ActionText ({ icon, className, text, onClick }: Props): React.ReactElement<Props> {
return (
<div className={className} onClick={onClick}>
{icon && <Svg src={icon} />}
<span>{text}</span>
</div>
);
}
export default styled(ActionText)`
cursor: pointer;
span {
font-size: ${({ theme }): string => theme.labelFontSize};
line-height: ${({ theme }): string => theme.labelLineHeight};
text-decoration-line: underline;
color: ${({ theme }): string => theme.labelColor}
}
${Svg} {
background: ${({ theme }): string => theme.iconNeutralColor};
display: inline-block;
position: relative;
top: 2px;
width: 14px;
height: 14px;
margin-right: 6px;
}
`;
@@ -0,0 +1,59 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React, { useContext } from 'react';
import styled from 'styled-components';
import { ActionContext } from './contexts';
import AddAccountImage from './AddAccountImage';
import Header from './Header';
interface Props {
className?: string;
}
function AddAccount ({ className }: Props): React.ReactElement<Props> {
const onAction = useContext(ActionContext);
return (
<>
<Header showSettings text={'Add Account'}/>
<div className={className}>
<Image>
<AddAccountImage onClick={(): void => onAction('/account/create')}/>
</Image>
<div>
<TipText>You currently don&apos;t have any accounts. Either create a new account or if you have an existing
account you wish to use, import it with the seed phrase.</TipText>
</div>
</div>
</>
);
}
const TipText = styled.p`
text-align: center;
font-size: 16px;
line-height: 26px;
margin: 0 30px;
color: ${({ theme }): string => theme.subTextColor};
`;
const Image = styled.div`
display: flex;
justify-content: center;
`;
export default styled(AddAccount)`
color: ${({ theme }): string => theme.textColor};
height: 100%;
h3 {
color: ${({ theme }): string => theme.textColor};
margin-top: 0;
font-weight: normal;
font-size: 24px;
line-height: 33px;
text-align: center;
}
`;
@@ -0,0 +1,77 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React from 'react';
import styled from 'styled-components';
interface Props {
onClick: () => void;
className?: string;
}
function AddAccountImage ({ onClick, className }: Props): React.ReactElement<Props> {
return (
<svg className={className} width="265" height="265" viewBox="0 0 265 265" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="mask0" mask-type="alpha" maskUnits="userSpaceOnUse" x="40" y="40" width="185" height="185">
<circle cx="132.5" cy="132.5" r="92" fill="white" fillOpacity="0.9" stroke="#E3E7ED" />
</mask>
<g mask="url(#mask0)">
<circle cx="132.5" cy="132.5" r="92" strokeWidth="1px" fill="#1A1B20" stroke="#E3E7ED"
onClick={onClick} />
<g filter="url(#filter0_f)">
<circle cx="132.5" cy="73.1154" r="12.1154" fill="#FEE05F" />
<circle cx="132.5" cy="104.269" r="12.1154" fill="#F5836B" />
<circle cx="132.5" cy="135.423" r="12.1154" fill="#EE6C89" />
<circle cx="132.5" cy="166.577" r="12.1154" fill="#397893" />
<circle cx="132.5" cy="166.577" r="12.1154" fill="#E2327D" />
<circle cx="77.1154" cy="104.269" r="12.1154" fill="#F0512A" />
<circle cx="77.1154" cy="135.423" r="12.1154" fill="#966EAC" />
<circle cx="77.1154" cy="166.577" r="12.1154" fill="#397893" />
<circle cx="77.1154" cy="166.577" r="12.1154" fill="#9195C9" />
<circle cx="187.884" cy="104.269" r="12.1154" fill="#FFFACA" />
<circle cx="187.884" cy="135.423" r="12.1154" fill="#ECCCDB" />
<circle cx="187.884" cy="166.577" r="12.1154" fill="#397893" />
<circle cx="187.884" cy="166.577" r="12.1154" fill="#ED3432" />
<circle cx="132.5" cy="197.731" r="12.1154" fill="#B24594" />
<circle cx="160.193" cy="86.9615" r="12.1154" fill="#FFBB50" />
<circle cx="160.193" cy="118.115" r="12.1154" fill="#F6E768" />
<circle cx="160.193" cy="149.269" r="12.1154" fill="#FF5A5A" />
<circle cx="160.193" cy="180.423" r="12.1154" fill="#397893" />
<circle cx="160.193" cy="180.423" r="12.1154" fill="#D6234A" />
<circle cx="104.808" cy="86.9615" r="12.1154" fill="#FFA800" />
<circle cx="104.808" cy="118.115" r="12.1154" fill="#F78D53" />
<circle cx="104.808" cy="149.269" r="12.1154" fill="#BD87BB" />
<circle cx="104.808" cy="180.423" r="12.1154" fill="#397893" />
<circle cx="104.808" cy="180.423" r="12.1154" fill="#AF71AF" />
</g>
<path
d="M145.768 133.776C146.323 133.776 146.792 133.968 147.176 134.352C147.603 134.736 147.816 135.205 147.816 135.76C147.816 136.315 147.624 136.805 147.24 137.232C146.856 137.616 146.365 137.808 145.768 137.808H134.184V149.328C134.184 149.925 133.992 150.416 133.608 150.8C133.224 151.184 132.755 151.376 132.2 151.376C131.645 151.376 131.176 151.184 130.792 150.8C130.408 150.373 130.216 149.883 130.216 149.328V137.808H118.632C118.077 137.808 117.587 137.616 117.16 137.232C116.776 136.805 116.584 136.315 116.584 135.76C116.584 135.205 116.776 134.736 117.16 134.352C117.587 133.968 118.077 133.776 118.632 133.776H130.216V122.256C130.216 121.659 130.408 121.168 130.792 120.784C131.176 120.357 131.645 120.144 132.2 120.144C132.755 120.144 133.224 120.357 133.608 120.784C133.992 121.168 134.184 121.659 134.184 122.256V133.776H145.768Z"
fill="#242529" onClick={onClick} />
</g>
<defs>
<filter id="filter0_f" x="-3" y="-7" width="271" height="284.846" filterUnits="userSpaceOnUse"
colorInterpolationFilters="sRGB">
<feFlood floodOpacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="10" result="effect1_foregroundBlur"/>
</filter>
</defs>
</svg>
);
}
export default styled(AddAccountImage)`
circle, path {
cursor: pointer;
}
path {
fill: ${({ theme }): string => theme.textColor};
}
& > g > circle {
stroke: ${({ theme }): string => theme.inputBorderColor};
fill: ${({ theme }): string => theme.addAccountImageBackground};
}
`;
+163 -33
View File
@@ -5,22 +5,26 @@
import { AccountJson } from '@polkadot/extension/background/types';
import { Chain } from '@polkadot/extension-chains/types';
import React, { useContext, useEffect, useState } from 'react';
import React, { useContext, useEffect, useRef, useState } from 'react';
import styled from 'styled-components';
import findChain from '@polkadot/extension-chains';
import Identicon from '@polkadot/react-identicon';
import settings from '@polkadot/ui-settings';
import { decodeAddress, encodeAddress } from '@polkadot/util-crypto';
import IconBox from './IconBox';
import { AccountContext } from './contexts';
import Identicon from '@polkadot/extension-ui/components/Identicon';
import Svg from '@polkadot/extension-ui/components/Svg';
import Menu from '@polkadot/extension-ui/components/Menu';
import DetailsImg from '../assets/details.svg';
import { useOutsideClick } from '@polkadot/extension-ui/hooks';
interface Props {
address?: string | null;
children?: React.ReactNode;
className?: string;
name?: React.ReactNode | null;
children?: React.ReactNode;
genesisHash?: string | null;
actions?: React.ReactNode;
}
// find an account in our list
@@ -49,17 +53,22 @@ function recodeAddress (address: string, accounts: AccountJson[], genesisHash?:
];
}
function Address ({ address, children, className, genesisHash, name }: Props): React.ReactElement<Props> {
const ACCOUNTS_SCREEN_HEIGHT = 500;
function Address ({ address, className, children, genesisHash, name, actions }: Props): React.ReactElement<Props> {
const accounts = useContext(AccountContext);
const [account, setAccount] = useState<AccountJson | null>(null);
const [chain, setChain] = useState<Chain | null>(null);
const [formatted, setFormatted] = useState<string | null>(null);
const [showActionsMenu, setShowActionsMenu] = useState(false);
const [moveMenuUp, setIsMovedMenu] = useState(false);
const actionsRef = useRef<HTMLDivElement>(null);
useOutsideClick(actionsRef, () => (showActionsMenu && setShowActionsMenu(!showActionsMenu)));
useEffect((): void => {
if (!address) {
return;
}
const [formatted, account, chain] = recodeAddress(address, accounts, genesisHash);
setFormatted(formatted);
@@ -67,40 +76,161 @@ function Address ({ address, children, className, genesisHash, name }: Props): R
setAccount(account);
}, [address]);
useEffect(() => {
if (!showActionsMenu) {
setIsMovedMenu(false);
return;
}
if (!actionsRef.current) {
return;
}
const { bottom } = actionsRef.current.getBoundingClientRect();
if (bottom > ACCOUNTS_SCREEN_HEIGHT) {
setIsMovedMenu(true);
}
}, [showActionsMenu]);
const theme = ((chain && chain.icon) || 'polkadot') as 'polkadot';
return (
<IconBox
banner={chain && chain.genesisHash && chain.name}
className={className}
icon={
<Identicon
className='icon'
size={64}
theme={theme}
value={address}
/>
}
intro={
<>
<div className='name'>{name || (account && account.name) || '<unknown>'}</div>
<div className='address'>{formatted || '<unknown>'}</div>
</>
}
>
{children}
</IconBox>
<div className={className}>
<div>
<AccountInfoRow>
<Identicon
iconTheme={theme}
value={address}
/>
<Info>
<Name>{name || (account && account.name) || '<unknown>'}</Name>
<FullAddress>{formatted || '<unknown>'}</FullAddress>
{chain?.genesisHash && <Banner>{chain.name}</Banner>}
</Info>
{actions && (
<>
<Settings onClick={(): void => setShowActionsMenu(!showActionsMenu)}>
{showActionsMenu ? <ActiveActionsIcon /> : <ActionsIcon />}
</Settings>
{showActionsMenu && <MovableMenu reference={actionsRef} isMoved={moveMenuUp}>{actions}</MovableMenu>}
</>
)}
</AccountInfoRow>
{children}
</div>
</div>
);
}
export default styled(Address)`
.address {
opacity: 0.5;
overflow: hidden;
text-overflow: ellipsis;
const AccountInfoRow = styled.div`
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
height: 72px;
border-radius: 4px;
`;
const Info = styled.div`
width: 100%;
`;
const Name = styled.div`
overflow: hidden;
text-overflow: ellipsis;
width: 300px;
margin: 2px 0;
font-weight: 600;
font-size: 16px;
line-height: 22px;
`;
const FullAddress = styled.div`
width: 300px;
overflow: hidden;
text-overflow: ellipsis;
color: ${({ theme }): string => theme.labelColor};
font-size: 12px;
line-height: 16px;
`;
FullAddress.displayName = 'FullAddress';
const Settings = styled.div`
position: relative;
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 40px;
& ${Svg} {
width: 3px;
height: 19px;
}
.name {
padding-bottom: 0.5rem;
&:before {
content: "";
position: absolute;
left: 0;
top: 25%;
bottom: 25%;
width: 1px;
background: ${({ theme }): string => theme.boxBorderColor};
}
&:hover {
cursor: pointer;
background: ${({ theme }): string => theme.readonlyInputBackground};
}
`;
Settings.displayName = 'Details';
const ActionsIcon = styled(Svg).attrs(() => ({
src: DetailsImg
}))`
background: ${({ theme }): string => theme.accountDotsIconColor};
`;
const ActiveActionsIcon = styled(Svg).attrs(() => ({
src: DetailsImg
}))`
background: ${({ theme }): string => theme.primaryColor};
`;
const Banner = styled.div`
background: ${({ theme }): string => theme.primaryColor};
border-radius: 0 0 8px 8px;
color: white;
font-size: 12px;
line-height: 16px;
padding: 0.1rem 0.5rem;
position: absolute;
right: 40px;
top: 0;
`;
const MovableMenu = styled(Menu)<{ isMoved: boolean }>`
${({ isMoved }): string => isMoved ? 'bottom: 50px' : ''};
`;
export default styled(Address)`
position: relative;
margin-bottom: 8px;
& > div {
background: ${({ theme }): string => theme.accountBackground};
border: 1px solid ${({ theme }): string => theme.boxBorderColor};
box-sizing: border-box;
border-radius: 4px;
}
& ${Identicon} {
margin-left: 25px;
margin-right: 10px;
& svg {
width: 50px;
height: 50px;
}
}
`;
+9 -12
View File
@@ -5,8 +5,6 @@
import React from 'react';
import styled from 'styled-components';
import defaults from './defaults';
interface Props {
banner?: React.ReactNode;
children: React.ReactNode;
@@ -23,20 +21,19 @@ function Box ({ banner, children, className }: Props): React.ReactElement<Props>
}
export default styled(Box)`
background: white;
border: ${defaults.boxBorder};
border-radius: ${defaults.borderRadius};
box-shadow: ${defaults.boxShadow};
color: ${defaults.color};
font-family: ${defaults.fontFamily};
font-size: ${defaults.fontSize};
margin: ${defaults.boxMargin};
padding: 0.75rem 1rem;
background: ${({ theme }): string => theme.readonlyInputBackground};
border: none;
border-radius: ${({ theme }): string => theme.borderRadius};
color: ${({ theme }): string => theme.textColor};
font-family: ${({ theme }): string => theme.fontFamily};
font-size: ${({ theme }): string => theme.fontSize};
margin: ${({ theme }): string => theme.boxMargin};
padding: ${({ theme }): string => theme.boxPadding};
position: relative;
.banner {
background: darkorange;
border-radius: 0 ${defaults.borderRadius} 0 ${defaults.borderRadius};
border-radius: 0 ${({ theme }): string => theme.borderRadius} 0 ${({ theme }): string => theme.borderRadius};
color: white;
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
+27 -31
View File
@@ -5,22 +5,16 @@
import React from 'react';
import styled from 'styled-components';
import defaults from './defaults';
interface Props {
className?: string;
children?: React.ReactNode;
isDanger?: boolean;
isDisabled?: boolean;
isSmall?: boolean;
label?: string;
onClick?: () => void | Promise<void | boolean>;
to?: string;
}
const DISABLED_OPACITY = '0.3';
function Button ({ children, className, isDisabled, label, onClick, to }: Props): React.ReactElement<Props> {
function Button ({ children, className, isDisabled, onClick, to }: Props): React.ReactElement<Props> {
const _onClick = (): void => {
if (isDisabled) {
return;
@@ -34,36 +28,38 @@ function Button ({ children, className, isDisabled, label, onClick, to }: Props)
};
return (
<div className={className}>
<button onClick={_onClick}>
{label}{children}
</button>
</div>
<button
className={className}
onClick={_onClick}
disabled={isDisabled}
>
{children}
</button>
);
}
export default styled(Button)`
display: block;
width: 100%;
height: ${({ isDanger }): string => isDanger ? '40px' : '48px'};
box-sizing: border-box;
display: ${({ isSmall }): string => isSmall ? 'inline-block' : 'block'};
margin: ${defaults.boxMargin};
padding: ${defaults.boxPadding};
width: ${({ isSmall }): string => isSmall ? 'auto' : '100%'};
border: none;
border-radius: ${({ theme }): string => theme.borderRadius};
color: ${({ theme }): string => theme.buttonTextColor};
font-size: 15px;
font-weight: 800;
line-height: 20px;
padding: 0 1rem;
text-align: center;
background: ${({ isDanger, theme }): string => isDanger ? theme.buttonBackgroundDanger : theme.buttonBackground};
cursor: pointer;
button {
background: ${({ isDanger }): string => isDanger ? defaults.btnBgDanger : defaults.btnBg};
border: ${defaults.btnBorder}${({ isDanger }): string => isDanger ? defaults.btnColorDanger : defaults.btnColor};
border-radius: ${defaults.borderRadius};
color: ${({ isDanger }): string => isDanger ? defaults.btnColorDanger : defaults.btnColor};
cursor: pointer;
display: block;
font-size: ${defaults.fontSize};
opacity: ${({ isDisabled }): string => isDisabled ? DISABLED_OPACITY : '0.8'};
padding: ${defaults.btnPadding};
text-align: center;
width: 100%;
&:disabled {
opacity: 0.3;
cursor: default;
}
&:hover {
opacity: ${({ isDisabled }): string => isDisabled ? DISABLED_OPACITY : '1.0'};
}
&:not(:disabled):hover {
background: ${({ isDanger, theme }): string => isDanger ? theme.buttonBackgroundDangerHover : theme.buttonBackgroundHover};
}
`;
@@ -0,0 +1,23 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import styled from 'styled-components';
import Button from './Button';
export default styled.div`
display: flex;
flex-direction: row;
background: ${({ theme }): string => theme.highlightedAreaBackground};
border-top: 1px solid ${({ theme }): string => theme.inputBorderColor};
padding: 12px 24px;
&&& {
margin-left: 0;
margin-right: 0;
}
& > ${Button}:not(:last-of-type) {
margin-right: 8px;
}
`;
@@ -0,0 +1,45 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React from 'react';
import { Button } from '.';
import styled from 'styled-components';
interface ButtonWithSubtitleProps {
title: string;
subTitle: string;
children?: string;
to: string;
}
export default function ButtonWithSubtitle ({ title, subTitle, children, to }: ButtonWithSubtitleProps): React.ReactElement<ButtonWithSubtitleProps> {
return (
<StyledButton to={to}>
<p>{title}</p>
<span>{subTitle}</span>
{children}
</StyledButton>
);
}
const StyledButton = styled(Button)`
button {
padding-top: 0;
padding-bottom: 0;
}
p {
margin: 0;
font-weight: 800;
font-size: 15px;
line-height: 20px;
}
span {
display: block;
font-weight: 600;
font-size: 12px;
line-height: 16px;
}
`;
@@ -0,0 +1,84 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React from 'react';
import styled from 'styled-components';
import Checkmark from '../assets/checkmark.svg';
interface Props {
checked: boolean;
onChange: (checked: boolean) => void;
label: string;
className?: string;
}
function Checkbox ({ onChange, label, className, checked }: Props): React.ReactElement<Props> {
return (
<div className={className}>
<label>
{label}
<input
type="checkbox"
checked={checked}
onChange={((event): void => onChange(event.target.checked))}
/>
<span />
</label>
</div>
);
}
export default styled(Checkbox)`
label {
display: block;
position: relative;
cursor: pointer;
user-select: none;
margin: ${({ theme }): string => theme.boxMargin};
padding-left: 24px;
color: ${({ theme }): string => theme.subTextColor};
font-size: ${({ theme }): string => theme.fontSize};
line-height: ${({ theme }): string => theme.lineHeight};
& input {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
}
& span {
position: absolute;
top: 4px;
left: 0;
height: 16px;
width: 16px;
border-radius: ${({ theme }): string => theme.borderRadius};
background-color: ${({ theme }): string => theme.readonlyInputBackground};
border: 1px solid ${({ theme }): string => theme.inputBorderColor};
border: 1px solid ${({ theme }): string => theme.inputBorderColor};
&:after {
content: "";
display: none;
width: 13px;
height: 10px;
position: absolute;
left: 1px;
top: 2px;
mask: url(${Checkmark});
mask-size: cover;
background: ${({ theme }): string => theme.primaryColor};
}
}
&:hover input ~ span {
background-color: ${({ theme }): string => theme.inputBackground};
}
input:checked ~ span:after {
display: block;
}
}
`;
@@ -4,11 +4,11 @@
import React from 'react';
import styled from 'styled-components';
import arrow from '../assets/arrow-down.svg';
import Label from './Label';
import defaults from './defaults';
interface DrodownOption {
interface DropdownOption {
text: string;
value: string;
}
@@ -18,11 +18,10 @@ interface Props {
defaultValue?: string | null;
isError?: boolean;
isFocussed?: boolean;
isReadOnly?: boolean;
label?: string | null;
label: string;
onBlur?: () => void;
onChange?: (value: string) => void;
options: DrodownOption[];
options: DropdownOption[];
value?: string;
}
@@ -31,54 +30,70 @@ function Dropdown ({ className, defaultValue, label, isFocussed, onBlur, onChang
onChange && onChange(value.trim());
};
console.error(value);
return (
<Label
className={className}
label={label}
>
<select
autoFocus={isFocussed}
defaultValue={defaultValue || undefined}
onBlur={onBlur}
onChange={_onChange}
value={value}
<>
<Label
className={className}
label={label}
>
{options.map(({ text, value }): React.ReactNode => (
<option
key={value}
value={value}
>
{text}
</option>
))}
</select>
</Label>
<select
autoFocus={isFocussed}
defaultValue={defaultValue || undefined}
onBlur={onBlur}
onChange={_onChange}
value={value}
>
{options.map(({ text, value }): React.ReactNode => (
<option
key={value}
value={value}
>
{text}
</option>
))}
</select>
</Label>
</>
);
}
export default styled(Dropdown)`
position: relative;
select {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background: ${({ isError, isReadOnly }): string => isError ? defaults.box.error.background : (isReadOnly ? '#eee' : '#fff')};
border-color: ${({ isError }): string => isError ? defaults.box.error.border : defaults.inputBorder};
border-radius: ${defaults.borderRadius};
background: ${({ theme }): string => theme.readonlyInputBackground};
border-color: ${({ isError, theme }): string => isError ? theme.errorBorderColor : theme.inputBorderColor};
border-radius: ${({ theme }): string => theme.borderRadius};
border-style: solid;
border-width: 1px;
box-sizing: border-box;
color: ${({ isError }): string => isError ? defaults.box.error.border : defaults.color};
color: ${({ isError, theme }): string => isError ? theme.errorBorderColor : theme.textColor};
display: block;
font-family: ${defaults.fontFamily};
font-size: ${defaults.fontSize};
padding: ${({ label }): string => label ? defaults.inputPaddingLabel : defaults.inputPadding};
font-family: ${({ theme }): string => theme.fontFamily};
font-size: ${({ theme }): string => theme.fontSize};
font-weight: 600;
padding: 0.5rem 0.75rem;
width: 100%;
cursor: pointer;
&:read-only {
box-shadow: none;
outline: none;
}
}
label::after {
content: '';
position: absolute;
top: 50%;
transform: translateY(-50%);
right: 5px;
width: 8px;
height: 6px;
background: url(${arrow}) center no-repeat;
pointer-events: none;
}
`;
@@ -0,0 +1,35 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { createGlobalStyle } from 'styled-components';
import NunitosansregularWoff2 from '../assets/fonts/Nunitosansregular.woff2';
import NunitosansregularWoff from '../assets/fonts/Nunitosansregular.woff';
import NunitosanssemiboldWoff2 from '../assets/fonts/Nunitosanssemibold.woff2';
import NunitosanssemiboldWoff from '../assets/fonts/Nunitosanssemibold.woff';
import NunitosanslightWoff2 from '../assets/fonts/Nunitosanslight.woff2';
import NunitosanslightWoff from '../assets/fonts/Nunitosanslight.woff';
export default createGlobalStyle`
@font-face {
font-family: 'Nunito';
src: url(${NunitosanslightWoff2}) format('woff2'),
url(${NunitosanslightWoff}) format('woff');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'Nunito';
src: url(${NunitosansregularWoff2}) format('woff2'),
url(${NunitosansregularWoff}) format('woff');
font-weight: 600;
font-style: normal;
}
@font-face {
font-family: 'Nunito';
src: url(${NunitosanssemiboldWoff2}) format('woff2'),
url(${NunitosanssemiboldWoff}) format('woff');
font-weight: 800;
font-style: normal;
}
`;
@@ -0,0 +1,40 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import Adapter from 'enzyme-adapter-react-16';
import { configure, mount, ReactWrapper } from 'enzyme';
import { themes, Header, Settings } from '@polkadot/extension-ui/components';
import { MemoryRouter } from 'react-router';
import React from 'react';
import { ThemeProvider } from 'styled-components';
configure({ adapter: new Adapter() });
describe('Header component', () => {
let wrapper: ReactWrapper;
const mountHeader = (props: React.ComponentProps<typeof Header> = {}): ReactWrapper => mount(
<MemoryRouter>
<ThemeProvider theme={themes.dark}>
<Header {...props}>
</Header>
</ThemeProvider>
</MemoryRouter>);
it('gear icon is not highlighted when settings are hidden', () => {
wrapper = mountHeader({ showSettings: true });
expect(wrapper.find(Settings).length).toBe(0);
expect(wrapper.find('Gear').prop('isSelected')).toBe(false);
});
it('highlights gear icon when settings are toggled', () => {
wrapper = mountHeader({ showSettings: true });
wrapper.find('SettingsToggle').simulate('click');
expect(wrapper.find(Settings).length).toBe(1);
expect(wrapper.find('Gear').prop('isSelected')).toBe(true);
});
});
+123 -30
View File
@@ -2,48 +2,141 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React from 'react';
import React, { useRef, useState } from 'react';
import styled from 'styled-components';
import logo from '../assets/pjs.svg';
import gear from '../assets/gear.svg';
import { useOutsideClick } from '../hooks';
import defaults from './defaults';
import Svg from '@polkadot/extension-ui/components/Svg';
import Settings from './Settings';
import ArrowLeftImage from '../assets/arrowLeft.svg';
import Link from './Link';
interface Props {
children?: React.ReactNode;
className?: string;
label?: React.ReactNode;
labelExtra?: React.ReactNode;
showSettings?: boolean;
text?: string;
showBackArrow?: boolean;
}
function Header ({ children, className, label, labelExtra }: Props): React.ReactElement<Props> {
function Header ({ children, className, showSettings, text, showBackArrow }: Props): React.ReactElement<Props> {
const [showActionsMenu, setShowActionsMenu] = useState(false);
const actionsRef = useRef(null);
useOutsideClick(actionsRef, () => (showActionsMenu && setShowActionsMenu(!showActionsMenu)));
return (
<h2 className={className}>
<div className='label'>{label}</div>
{labelExtra && (
<div className='labelExtra'>{labelExtra}</div>
)}
{children}
</h2>
<div className={className}>
<Container>
<Branding>
{showBackArrow ? (
<BackLink to={'/'}>
<ArrowLeft/>
</BackLink>
) : <Logo src={logo}/>
}
<LogoText>{text || 'polkadot{.js}'}</LogoText>
</Branding>
{showSettings && (
<SettingsToggle onClick={(): void => setShowActionsMenu(!showActionsMenu)}>
<Gear isSelected={showActionsMenu} />
</SettingsToggle>
)}
{showActionsMenu && (
<Settings reference={actionsRef}/>
)}
{children}
</Container>
</div>
);
}
export default styled(Header)`
background: ${defaults.hdrBg};
box-sizing: border-box;
font-weight: normal;
margin: 0 -1rem;
padding: 0.75rem 1rem;
position: relative;
const BackLink = styled(Link)`
width: min-content;
text-decoration: underline;
color: ${({ theme }): string => theme.labelColor};
min-height: 52px;
.label {
color: ${defaults.hdrColor};
font-family: ${defaults.fontFamily};
text-transform: uppercase;
}
.labelExtra {
bottom: 0.75rem;
font-size: 1rem;
position: absolute;
right: 1rem;
&:visited {
color: ${({ theme }): string => theme.labelColor};
}
`;
const ArrowLeft = styled(Svg).attrs(() => ({
src: ArrowLeftImage
}))`
width: 12px;
height: 12px;
margin-right: 13px;
background: ${({ theme }): string => theme.labelColor};
`;
const Container = styled.div`
display: flex;
justify-content: space-between;
width: 100%;
border-bottom: 1px solid ${({ theme }): string => theme.inputBorderColor};
min-height: 70px;
`;
const Branding = styled.div`
display: flex;
justify-content: center;
align-items: center;
color: ${({ theme }): string => theme.labelColor};
font-family: ${({ theme }): string => theme.fontFamily};
text-align: center;
margin-left: 24px;
`;
const Logo = styled.img`
height: 28px;
width: 28px;
margin: 8px 12px 12px 0;
`;
interface GearProps {
isSelected: boolean;
}
const Gear = styled(Svg).attrs(() => ({
src: gear
}))<GearProps>`
height: 18px;
width: 18px;
margin-right: 24px;
align-self: center;
background: ${({ theme, isSelected }): string => isSelected ? theme.primaryColor : theme.iconNeutralColor};
`;
Gear.displayName = 'Gear';
const LogoText = styled.span`
color: ${({ theme }): string => theme.textColor};
font-family: ${({ theme }): string => theme.fontFamily};
font-size: 20px;
line-height: 27px;
`;
const SettingsToggle = styled.div`
align-self: center;
&:hover {
cursor: pointer;
}
`;
SettingsToggle.displayName = 'SettingsToggle';
export default styled(Header)`
max-width: 100%;
box-sizing: border-box;
font-weight: normal;
margin: 0;
position: relative;
margin-bottom: 25px;
&& {
padding: 0 0 0;
}
`;
@@ -1,62 +0,0 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React from 'react';
import styled from 'styled-components';
import Box from './Box';
import defaults from './defaults';
interface Props {
banner?: React.ReactNode;
children?: React.ReactNode;
className?: string;
icon: React.ReactNode;
intro: React.ReactNode;
name?: React.ReactNode | null;
theme?: 'polkadot' | 'substrate';
}
function IconBox ({ banner, children, className, icon, intro }: Props): React.ReactElement<Props> {
return (
<div className={className}>
<Box
banner={banner}
className='details'
>
<div className='intro'>{intro}</div>
<div className='children'>{children}</div>
</Box>
<div className='outer-icon'>{icon}</div>
</div>
);
}
export default styled(IconBox)`
box-sizing: border-box;
margin: ${defaults.boxMargin};
padding: ${defaults.boxPadding};
padding-left: 1rem;
position: relative;
.details {
margin: 0;
.intro {
padding-left: 3rem;
}
}
.outer-icon {
height: 64px;
font-size: 36px;
left: 0.25rem;
line-height: 64px;
position: absolute;
top: -0.5rem;
vertical-align: middle;
width: 64px;
z-index: 1;
}
`;
@@ -0,0 +1,42 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React from 'react';
import styled from 'styled-components';
import Icon from '@polkadot/react-identicon';
interface Props {
iconTheme?: 'beachball' | 'empty' | 'jdenticon' | 'polkadot' | 'substrate';
className?: string;
value?: string | null;
}
function Identicon ({ iconTheme, className, value }: Props): React.ReactElement<Props> {
return (
<div className={className}>
<Icon
className='icon'
size={64}
theme={iconTheme}
value={value}
/>
</div>
);
}
export default styled(Identicon)`
display: flex;
justify-content: center;
.container:before {
box-shadow: none;
background: ${({ theme }): string => theme.identiconBackground};
}
svg {
circle:first-of-type {
display: none;
}
}
`;
@@ -1,67 +0,0 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React from 'react';
import styled from 'styled-components';
import Label from './Label';
import defaults from './defaults';
interface Props {
className?: string;
defaultValue?: string | null;
isError?: boolean;
isFocussed?: boolean;
isReadOnly?: boolean;
label?: string | null;
onBlur?: () => void;
onChange?: (value: string) => void;
type?: 'text' | 'password';
value?: string;
}
function Input ({ className, defaultValue, label, isFocussed, isReadOnly, onBlur, onChange, type = 'text', value }: Props): React.ReactElement<Props> {
const _onChange = ({ target: { value } }: React.ChangeEvent<HTMLInputElement>): void => {
onChange && onChange(value.trim());
};
return (
<Label
className={className}
label={label}
>
<input
autoFocus={isFocussed}
defaultValue={defaultValue || undefined}
readOnly={isReadOnly}
onBlur={onBlur}
onChange={_onChange}
type={type}
value={value}
/>
</Label>
);
}
export default styled(Input)`
input {
background: ${({ isError, isReadOnly }): string => isError ? defaults.box.error.background : (isReadOnly ? '#eee' : '#fff')};
border-color: ${({ isError }): string => isError ? defaults.box.error.border : defaults.inputBorder};
border-radius: ${defaults.borderRadius};
border-style: solid;
border-width: 1px;
box-sizing: border-box;
color: ${({ isError }): string => isError ? defaults.box.error.border : defaults.color};
display: block;
font-family: ${defaults.fontFamily};
font-size: ${defaults.fontSize};
padding: ${({ label }): string => label ? defaults.inputPaddingLabel : defaults.inputPadding};
width: 100%;
&:read-only {
box-shadow: none;
outline: none;
}
}
`;
@@ -0,0 +1,50 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React from 'react';
import Label from './Label';
import styled from 'styled-components';
import { Input } from './TextInputs';
interface Props {
className?: string;
defaultValue?: string | null;
isError?: boolean;
isFocused?: boolean;
isReadOnly?: boolean;
label: string;
onBlur?: () => void;
onChange?: (value: string) => void;
type?: 'text' | 'password';
value?: string;
}
function InputWithLabel ({ className, defaultValue, label, isFocused, isReadOnly, onBlur, onChange, type = 'text', value, isError }: Props): React.ReactElement<Props> {
const _onChange = ({ target: { value } }: React.ChangeEvent<HTMLInputElement>): void => {
onChange && onChange(value.trim());
};
return (
<Label
className={className}
label={label}
>
<Input
withError={isError}
autoFocus={isFocused}
defaultValue={defaultValue || undefined}
readOnly={isReadOnly}
onBlur={onBlur}
onChange={_onChange}
type={type}
value={value}
/>
</Label>
);
}
export default styled(InputWithLabel)`
margin-bottom: 16px;
`;
+10 -17
View File
@@ -5,38 +5,31 @@
import React from 'react';
import styled from 'styled-components';
import defaults from './defaults';
interface Props {
children: React.ReactNode;
className?: string;
label?: string | null;
label: string;
}
function Label ({ children, className, label }: Props): React.ReactElement<Props> {
return (
<div className={className}>
{label && <label>{label}</label>}
<label>{label}</label>
{children}
</div>
);
}
export default styled(Label)`
box-sizing: border-box;
color: ${defaults.labelColor};
display: block;
font-family: ${defaults.fontFamily};
font-size: ${defaults.fontSize};
margin: ${defaults.boxMargin};
padding: ${defaults.boxPadding};
position: relative;
color: ${({ theme }): string => theme.textColor};
label {
display: block;
font-size: 0.75rem;
left: 1rem;
position: absolute;
top: 0.25rem;
font-size: 10px;
line-height: 14px;
letter-spacing: 0.04em;
font-weight: 800;
opacity: 0.65;
margin-bottom: 12px;
text-transform: uppercase;
}
`;
+22 -8
View File
@@ -6,8 +6,6 @@ import React from 'react';
import styled from 'styled-components';
import { Link as RouterLink } from 'react-router-dom';
import defaults from './defaults';
interface Props {
children?: React.ReactNode;
className?: string;
@@ -18,24 +16,40 @@ interface Props {
function Link ({ children, className, onClick, to }: Props): React.ReactElement<Props> {
return (
to
? <RouterLink className={className} onClick={onClick} to={to}>{children}</RouterLink>
: <a href='#' className={className} onClick={onClick}>{children}</a>
to ? (
<RouterLink
className={className}
onClick={onClick}
to={to}
>
{children}
</RouterLink>
) : (
<a
href='#'
className={className}
onClick={onClick}
>
{children}
</a>
)
);
}
export default styled(Link)`
color: ${({ isDanger }): string => isDanger ? defaults.linkColorDanger : defaults.linkColor};
display: flex;
align-items: center;
color: ${({ isDanger, theme }): string => isDanger ? theme.textColorDanger : theme.textColor};
opacity: 0.9;
text-decoration: none;
vertical-align: middle;
&:hover {
color: ${({ isDanger }): string => isDanger ? defaults.linkColorDanger : defaults.linkColor};
color: ${({ isDanger, theme }): string => isDanger ? theme.textColorDanger : theme.textColor};
opacity: 1.0;
}
&:visited {
color: ${({ isDanger }): string => isDanger ? defaults.linkColorDanger : defaults.linkColor};
color: ${({ isDanger, theme }): string => isDanger ? theme.textColorDanger : theme.textColor};
}
`;
@@ -0,0 +1,26 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import styled from 'styled-components';
export default styled.ul`
list-style: none;
padding-inline-start: 10px;
padding-inline-end: 10px;
text-indent: -22px;
margin-left: 21px;
li {
margin-bottom: 8px;
}
li::before {
content: '\\2022';
color: ${({ theme }): string => theme.primaryColor};
font-size: 30px;
font-weight: bold;
margin-right: 10px;
vertical-align: -20%;
}
`;
@@ -0,0 +1,33 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React from 'react';
import styled from 'styled-components';
interface Props {
children: React.ReactNode;
className?: string;
reference: React.RefObject<HTMLDivElement>;
}
function Menu ({ children, className, reference }: Props): React.ReactElement<Props> {
return (
<div className={className} ref={reference}>
{children}
</div>
);
}
export default styled(Menu)`
position: absolute;
right: 0;
margin-top: 90px;
padding: 16px 0;
background: ${({ theme }): string => theme.popupBackground};
border-radius: 4px;
border: 1px solid ${({ theme }): string => theme.boxBorderColor};
box-sizing: border-box;
box-shadow: 0 0 32px ${({ theme }): string => theme.boxShadow};
z-index: 1;
`;
@@ -0,0 +1,59 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import styled from 'styled-components';
import React, { MouseEventHandler } from 'react';
import TextAreaWithLabel from './TextAreaWithLabel';
import ActionText from '@polkadot/extension-ui/components/ActionText';
import copy from '../assets/copy.svg';
interface Props {
seed: string;
onCopy: MouseEventHandler<HTMLDivElement>;
className?: string;
}
function MnemonicSeed ({ seed, onCopy, className }: Props): React.ReactElement<Props> {
return (
<div className={className}>
<MnemonicText value={seed} />
<ButtonsRow>
<ActionText
data-seed-action='copy'
icon={copy}
text='Copy to clipboard'
onClick={onCopy}
/>
</ButtonsRow>
</div>
);
}
const MnemonicText = styled(TextAreaWithLabel).attrs(() => ({
label: 'Generated 12-word mnemonic seed:',
isReadOnly: true
}))`
textarea {
font-size: ${({ theme }): string => theme.fontSize};
line-height: ${({ theme }): string => theme.lineHeight};
height: unset;
letter-spacing: -0.01em;
padding: 14px;
margin-bottom: 10px;
color: ${({ theme }): string => theme.primaryColor};
}
`;
const ButtonsRow = styled.div`
display: flex;
flex-direction: row;
${ActionText} {
margin-right: 32px;
}
`;
export default styled(MnemonicSeed)`
margin-top: 21px;
`;
@@ -0,0 +1,152 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React, { useContext, useState, useEffect } from 'react';
import Menu from './Menu';
import Title from './Title';
import Checkbox from './Checkbox';
import ActionText from './ActionText';
import Svg from './Svg';
import Switch from './Switch';
import { themes } from './themes';
import { ThemeSwitchContext } from './contexts';
import { setSS58Format } from '@polkadot/util-crypto';
import settings from '@polkadot/ui-settings';
import styled, { ThemeContext } from 'styled-components';
import Dropdown from './Dropdown';
import { windowOpen } from '../messaging';
import FullScreenIcon from '../assets/fullscreen.svg';
interface Option {
text: string;
value: string;
}
const isPopup = window.innerWidth <= 480;
const prefixOptions = settings.availablePrefixes.map(({ text, value }): Option => ({
text: value === -1
? 'Default'
: text,
value: `${value}`
}));
export default function Settings ({ reference }: { reference: React.MutableRefObject<null> }): React.ReactElement {
const [camera, setCamera] = useState(settings.camera === 'on');
const [prefix, setPrefix] = useState(`${settings.prefix}`);
const themeContext = useContext(ThemeContext);
const setTheme = useContext(ThemeSwitchContext);
useEffect(() => {
if (camera) {
settings.set({ camera: 'on' });
} else {
settings.set({ camera: 'off' });
}
}, [camera]);
// FIXME check against index, we need a better solution
const _onChangePrefix = (value: string): void => {
const prefix = parseInt(value, 10);
setSS58Format(prefix === -1 ? 42 : prefix);
setPrefix(value);
settings.set({ prefix });
location.reload();
};
return (
<SettingsMenu reference={reference}>
<Setting>
<SettingTitle>Theme</SettingTitle>
<Switch
checked={themeContext.id === themes.dark.id}
uncheckedLabel='Light'
checkedLabel='Dark'
onChange={(checked): void => setTheme(checked ? 'dark' : 'light')}
/>
</Setting>
<Setting>
<SettingTitle>External QR accounts and Access</SettingTitle>
<CheckboxSetting
checked={camera}
onChange={setCamera}
label='Allow Camera Access'
/>
</Setting>
<Setting>
<SettingTitle>Display address format For:</SettingTitle>
<DropdownSetting
label=''
onChange={_onChangePrefix}
options={prefixOptions}
value={`${prefix}`}
/>
</Setting>
{isPopup && (
<Setting>
<OpenInNewWindowButton
onClick={windowOpen}
text='Open extension in new window'
icon={FullScreenIcon}
/>
</Setting>
)}
</SettingsMenu>
);
}
const SettingsMenu = styled(Menu)`
margin-top: 56px;
right: 24px;
user-select: none;
`;
const CheckboxSetting = styled(Checkbox)`
font-size: 15px;
font-weight: 600;
line-height: 20px;
color: ${({ theme }): string => theme.textColor};
label {
color: ${({ theme }): string => theme.textColor};
}
`;
const DropdownSetting = styled(Dropdown)`
background: ${({ theme }): string => theme.background};
margin-top: 9px;
margin-bottom: 12px;
width : 100%;
margin-right: 0;
`;
const SettingTitle = styled(Title)`
margin: 0;
`;
const Setting = styled.div`
padding: 0 16px;
max-width: 100%;
& + & {
padding-top: 18px;
border-top: 1px solid ${({ theme }): string => theme.inputBorderColor};
}
`;
const OpenInNewWindowButton = styled(ActionText)`
span {
color: ${({ theme }): string => theme.textColor};
text-decoration: none;
font-weight: 600;
font-size: ${({ theme }): string => theme.fontSize};
line-height: ${({ theme }): string => theme.lineHeight};
}
${Svg} {
width: 20px;
height: 20px;
top: 4px;
background: ${({ theme }): string => theme.textColor};
}
`;
@@ -0,0 +1,16 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import styled from 'styled-components';
interface Props {
src: string;
}
export default styled.span<Props>`
display: block;
mask: ${({ src }): string => `url(${src})`};
mask-size: cover;
background: ${({ theme }): string => theme.textColor};
`;
@@ -0,0 +1,77 @@
import React from 'react';
import styled from 'styled-components';
interface Props {
checked: boolean;
onChange: (checked: boolean) => void;
uncheckedLabel: string;
checkedLabel: string;
className?: string;
}
function Switch ({ onChange, uncheckedLabel, checkedLabel, className, checked }: Props): React.ReactElement<Props> {
return (
<div className={className}>
<span>{uncheckedLabel}</span>
<label>
<Checkbox
checked={checked}
onChange={((event): void => onChange(event.target.checked))}
/>
<Slider />
</label>
<span>{checkedLabel}</span>
</div>
);
}
const Slider = styled.span`
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: ${({ theme }): string => theme.readonlyInputBackground};
transition: 0.2s;
border-radius: 100px;
border: 1px solid ${({ theme }): string => theme.inputBorderColor};
&:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 4px;
bottom: 3px;
background-color: ${({ theme }): string => theme.primaryColor};
transition: 0.4s;
border-radius: 50%;
}
`;
const Checkbox = styled.input.attrs(() => ({
type: 'checkbox'
}))`
opacity: 0;
width: 0;
height: 0;
&:checked + ${Slider}:before {
transform: translateX(24px);
}
`;
export default styled(Switch)`
label {
position: relative;
display: inline-block;
width: 48px;
height: 24px;
margin: 8px;
}
span {
font-weight: 600;
}
`;
@@ -1,64 +0,0 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React from 'react';
import styled from 'styled-components';
import Label from './Label';
import defaults from './defaults';
interface Props {
className?: string;
isError?: boolean;
isFocussed?: boolean;
isReadOnly?: boolean;
rowsCount?: number;
label: string;
onChange?: (value: string) => void;
value?: string;
}
function TextArea ({ className, isFocussed, isReadOnly, label, onChange, value, rowsCount }: Props): React.ReactElement<Props> {
const _onChange = ({ target: { value } }: React.ChangeEvent<HTMLTextAreaElement>): void => {
onChange && onChange(value.trim());
};
return (
<Label
className={className}
label={label}
>
<textarea
autoFocus={isFocussed}
onChange={_onChange}
readOnly={isReadOnly}
value={value}
rows={rowsCount || 2}
/>
</Label>
);
}
export default styled(TextArea)`
textarea {
background: ${({ isError, isReadOnly }): string => isError ? defaults.box.error.background : (isReadOnly ? '#eee' : '#fff')};
border-color: ${({ isError }): string => isError ? defaults.box.error.border : defaults.inputBorder};
border-radius: ${defaults.borderRadius};
border-style: solid;
border-width: 1px;
box-sizing: border-box;
color: ${({ isError }): string => isError ? defaults.box.error.border : defaults.color};
display: block;
font-family: ${defaults.fontFamily};
font-size: ${defaults.fontSize};
padding: ${({ label }): string => label ? defaults.inputPaddingLabel : defaults.inputPadding};
resize: none;
width: 100%;
&:read-only {
box-shadow: none;
outline: none;
}
}
`;
@@ -0,0 +1,43 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React from 'react';
import Label from './Label';
import { TextArea } from '@polkadot/extension-ui/components/TextInputs';
interface Props {
className?: string;
isError?: boolean;
isFocused?: boolean;
isReadOnly?: boolean;
rowsCount?: number;
label: string;
onChange?: (value: string) => void;
value?: string;
}
function TextAreaWithLabel ({ className, isFocused, isReadOnly, label, onChange, value, rowsCount, isError }: Props): React.ReactElement<Props> {
const _onChange = ({ target: { value } }: React.ChangeEvent<HTMLTextAreaElement>): void => {
onChange && onChange(value.trim());
};
return (
<Label
className={className}
label={label}
>
<TextArea
withError={isError}
autoFocus={isFocused}
onChange={_onChange}
readOnly={isReadOnly}
value={value}
rows={rowsCount || 2}
/>
</Label>
);
}
export default TextAreaWithLabel;
@@ -0,0 +1,46 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import styled, { css } from 'styled-components';
interface Props {
withError?: boolean;
}
const DefaultTextInputColors = css`
background: ${({ theme }): string => theme.inputBackground};
border-color: ${({ theme }): string => theme.inputBorderColor};
color: ${({ theme }): string => theme.textColor};
`;
const ErroredTextInputColors = css`
background: ${({ theme }): string => theme.inputBackground};
border-color: ${({ theme }): string => theme.errorBorderColor};
color: ${({ theme }): string => theme.errorColor};
`;
const TextInput = css<Props>`
border-radius: ${({ theme }): string => theme.borderRadius};
border: ${({ theme }): string => `1px solid ${theme.inputBorderColor}`};
outline: none;
box-sizing: border-box;
display: block;
font-family: ${({ theme }): string => theme.fontFamily};
font-size: ${({ theme }): string => theme.fontSize};
font-weight: 600;
height: 40px;
padding: 0.5rem 0.75rem;
resize: none;
width: 100%;
${({ withError }): typeof ErroredTextInputColors => (withError ? ErroredTextInputColors : DefaultTextInputColors)};
&:read-only {
background: ${({ theme }): string => theme.readonlyInputBackground};
box-shadow: none;
outline: none;
}
`;
export const TextArea = styled.textarea<Props>`${TextInput}`;
export const Input = styled.input<Props>`${TextInput}`;
@@ -1,50 +0,0 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React from 'react';
import styled from 'styled-components';
import defaults from './defaults';
interface Color {
background: string;
border: string;
color: string;
}
type Type = keyof typeof defaults.box;
interface Props {
children: React.ReactNode;
className?: string;
header?: React.ReactNode;
type?: Type;
}
function getColor ({ type }: Props): Color {
return defaults.box[type || 'info'] || defaults.box.info;
}
function Tip ({ children, className, header }: Props): React.ReactElement<Props> {
return (
<article className={className}>
{header && <h3>{header}</h3>}
<div>{children}</div>
</article>
);
}
// box-shadow: ${defaults.boxShadow};
export default styled(Tip)`
background: ${(p): string => getColor(p).background};
border-left: 0.25rem solid ${(p): string => getColor(p).border};
color: ${(p): string => getColor(p).color};
margin: 0.75rem -1rem;
padding: 1rem 1.5rem;
h3 {
color: ${(p): string => getColor(p).border};
font-weight: normal;
}
`;
@@ -0,0 +1,18 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import styled from 'styled-components';
export default styled.div`
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 }): string => theme.textColor};
opacity: 0.65;
`;
@@ -0,0 +1,9 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import styled from 'styled-components';
export default styled.div`
height: 100%;
`;
+50 -15
View File
@@ -2,10 +2,9 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React from 'react';
import styled from 'styled-components';
import defaults from './defaults';
import React, { useState } from 'react';
import styled, { ThemeProvider, createGlobalStyle } from 'styled-components';
import { AvailableThemes, chooseTheme, themes, ThemeSwitchContext, Theme } from '.';
interface Props {
children: React.ReactNode;
@@ -13,21 +12,57 @@ interface Props {
}
function View ({ children, className }: Props): React.ReactElement<Props> {
const [theme, setTheme] = useState(chooseTheme());
const switchTheme = (theme: AvailableThemes): void => {
localStorage.setItem('theme', theme);
setTheme(theme);
};
return (
<main className={className}>
{children}
</main>
<ThemeSwitchContext.Provider value={switchTheme}>
<ThemeProvider theme={themes[theme]}>
<BodyTheme theme={themes[theme]} />
<Main className={className}>
{children}
</Main>
</ThemeProvider>
</ThemeSwitchContext.Provider>
);
}
export default styled(View)`
color: ${defaults.color};
font-family: ${defaults.fontFamily};
font-size: ${defaults.fontSize};
line-height: ${defaults.lineHeight};
const BodyTheme = createGlobalStyle<{ theme: Theme }>`
body {
background-color: ${({ theme }): string => theme.bodyColor};
}
h3 {
margin: 0 0 0.75rem;
text-transform: uppercase;
html {
scrollbar-width: none;
&::-webkit-scrollbar {
display: none;
}
}
`;
const Main = styled.main`
display: flex;
flex-direction: column;
height: 100%;
background: ${({ theme }): string => theme.background};
color: ${({ theme }): string => theme.textColor};
font-size: ${({ theme }): string => theme.fontSize};
line-height: ${({ theme }): string => theme.lineHeight};
border: 1px solid ${({ theme }): string => theme.inputBorderColor};
* {
font-family: ${({ theme }): string => theme.fontFamily};
}
> * {
padding-left: 24px;
padding-right: 24px;
}
`;
export default View;
@@ -0,0 +1,43 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React from 'react';
import styled from 'styled-components';
import WarningImageSrc from '../assets/warning.svg';
import Svg from '@polkadot/extension-ui/components/Svg';
interface Props {
children: React.ReactNode;
danger?: boolean;
className?: string;
}
function Warning ({ children, className, danger }: Props): React.ReactElement<Props> {
return (
<div className={className}>
<div>
<WarningImage danger={danger} src={WarningImageSrc} />
</div>
<div>
{children}
</div>
</div>
);
}
const WarningImage = styled(Svg)<Pick<Props, 'danger'>>`
width: 16px;
height: 14px;
margin: 5px 10px 5px 0;
background: ${({ danger, theme }): string => danger ? theme.iconDangerColor : theme.iconWarningColor};
`;
export default styled(Warning)`
display: flex;
flex-direction: row;
padding-left: ${({ danger }): string => danger ? '18px' : ''};
color: ${({ theme }): string => theme.subTextColor};
margin-right: 20px;
border-left: ${({ danger, theme }): string => danger ? `0.25rem solid ${theme.buttonBackgroundDanger}` : ''};
`;
@@ -5,20 +5,23 @@
import { AccountJson, AuthorizeRequest, SigningRequest } from '@polkadot/extension/background/types';
import React from 'react';
import { AvailableThemes } from '.';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const noop = (to?: string): void => undefined;
const noop = (): void => undefined;
const AccountContext = React.createContext<AccountJson[]>([]);
const ActionContext = React.createContext<(to?: string) => void>(noop);
const AuthorizeContext = React.createContext<AuthorizeRequest[]>([]);
const MediaContext = React.createContext<boolean>(false);
const SigningContext = React.createContext<SigningRequest[]>([]);
const ThemeSwitchContext = React.createContext<(theme: AvailableThemes) => void>(noop);
export {
AccountContext,
ActionContext,
AuthorizeContext,
MediaContext,
SigningContext
SigningContext,
ThemeSwitchContext
};
@@ -1,58 +0,0 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
const DANGER_COLOR = '#c00';
const LABEL_COLOR = '#878786';
const LINK_COLOR = '#3367d6';
const TEXT_COLOR = '#4d4e4f';
const defaults = {
borderRadius: '0.25rem',
btnBg: TEXT_COLOR, // LINK_COLOR,
btnBgDanger: DANGER_COLOR,
btnBorder: '0 solid ',
btnColor: '#f5f6f7',
btnColorDanger: '#f5f6f7',
btnPadding: '0.75rem 1rem',
boxBorder: 'none', // '0.25rem solid #e2e1e0',
boxMargin: '0.75rem 0',
boxPadding: '0 0.25rem',
boxShadow: '0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24)',
color: TEXT_COLOR,
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif',
fontSize: '1rem',
hdrBg: 'transparent', // '#f5f6f7',
hdrColor: LABEL_COLOR,
inputBorder: '#ccc',
inputPadding: '0.5rem 0.75rem',
inputPaddingLabel: '1.25rem 0.75rem 0.5rem',
labelColor: LABEL_COLOR,
lineHeight: '1.25',
linkColor: LINK_COLOR,
linkColorDanger: DANGER_COLOR,
box: {
error: {
background: '#ffe6e6',
border: DANGER_COLOR,
color: '#4d0000'
},
info: {
background: '#fafafa',
border: TEXT_COLOR,
color: 'inherit'
},
success: {
background: '#f3f5f7',
border: '#42b983',
color: 'inherit'
},
warn: {
background: '#fff6cb',
border: '#e7c000',
color: '#6b5900'
}
}
};
export default defaults;
+20 -5
View File
@@ -3,19 +3,34 @@
// of the Apache-2.0 license. See the LICENSE file for details.
export { default as ActionBar } from './ActionBar';
export { default as ActionText } from './ActionText';
export { default as AddAccount } from './AddAccount';
export { default as Address } from './Address';
export { default as Box } from './Box';
export { default as Button } from './Button';
export { default as ButtonArea } from './ButtonArea';
export { default as ButtonWithSubtitle } from './ButtonWithSubtitle';
export { default as Checkbox } from './Checkbox';
export { default as Dropdown } from './Dropdown';
export { default as Fonts } from './Fonts';
export { default as Header } from './Header';
export { default as Icon } from './Icon';
export { default as IconBox } from './IconBox';
export { default as Input } from './Input';
export { default as Identicon } from './Identicon';
export { default as InputWithLabel } from './InputWithLabel';
export { default as Link } from './Link';
export { default as List } from './List';
export { default as Loading } from './Loading';
export { default as TextArea } from './TextArea';
export { default as Tip } from './Tip';
export { default as Menu } from './Menu';
export { default as MnemonicSeed } from './MnemonicSeed';
export { default as Settings } from './Settings';
export { default as Svg } from './Svg';
export { default as TextAreaWithLabel } from './TextAreaWithLabel';
export { default as Title } from './Title';
export { default as VerticalSpace } from './VerticalSpace';
export { default as View } from './View';
export { default as Warning } from './Warning';
export { Input, TextArea } from './TextInputs';
export * from './contexts';
export { default as defaults } from './defaults';
export * from './themes';
@@ -0,0 +1,85 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
const darkTheme = {
id: 'dark',
background: '#26272C',
bodyColor: '#20222A',
highlightedAreaBackground: '#212226',
popupBackground: '#38393F',
accountBackground: '#1A1B20',
buttonBackground: '#E86F00',
buttonBackgroundHover: '#ED9329',
buttonBackgroundDanger: '#AF1111',
buttonBackgroundDangerHover: '#D93B3B',
buttonTextColor: '#FFFFFF',
textColor: '#FFFFFF',
subTextColor: '#DDD',
textColorDanger: '#FF8686',
errorBorderColor: '#7E3530',
errorColor: '#E42F2F',
primaryColor: '#FF7D01',
inputBackground: '#111218',
inputBorderColor: '#43444B',
readonlyInputBackground: '#1A1B20',
boxBorderColor: '#43444B',
identiconBackground: '#F4F5F8',
accountDotsIconColor: '#8E8E8E',
iconWarningColor: '#FF7D01',
iconDangerColor: '#AF1111',
iconNeutralColor: '#8E8E8E',
labelColor: '#9F9E99',
addAccountImageBackground: '#1A1B20',
boxShadow: 'rgba(0, 0, 0, 0.86)',
fontFamily: 'Nunito, sans-serif',
fontSize: '16px',
lineHeight: '26px',
labelFontSize: '13px',
labelLineHeight: '18px',
borderRadius: '4px',
boxMargin: '0.75rem 0',
boxPadding: '0 0.25rem'
};
const lightTheme: Theme = {
...darkTheme,
id: 'light',
background: '#FAFAFA',
bodyColor: '#FFFFFF',
highlightedAreaBackground: '#EFEFEF',
accountBackground: '#FFFFFF',
popupBackground: '#FFFFFF',
textColor: '#242529',
subTextColor: '#454545',
errorBorderColor: '#E42F2F',
textColorDanger: '#F24A4A',
buttonBackgroundDanger: '#DC2222',
iconDangerColor: '#DC2222',
inputBackground: '#FFFFFF',
inputBorderColor: '#DDE1EB',
readonlyInputBackground: '#FFF',
boxBorderColor: '#DADFEA',
iconNeutralColor: '#939CB1',
labelColor: '#333333',
addAccountImageBackground: '#FFF',
boxShadow: 'rgba(0, 0, 0, 0.06)'
};
export function chooseTheme (): AvailableThemes {
const preferredTheme = localStorage.getItem('theme');
if (preferredTheme) {
return preferredTheme === 'dark' ? 'dark' : 'light';
}
const isLightColorSchemePreferred = window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches;
return isLightColorSchemePreferred ? 'light' : 'dark';
}
export declare type Theme = typeof darkTheme;
export const themes = {
dark: darkTheme,
light: lightTheme
};
export declare type AvailableThemes = keyof typeof themes;
+3 -3
View File
@@ -1,12 +1,11 @@
// Copyright 2019 @polkadot/extension authors & contributors
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React, { Suspense } from 'react';
import ReactDOM from 'react-dom';
import { HashRouter } from 'react-router-dom';
import { View } from './components';
import { Fonts, View } from './components';
export default function createView (Entry: React.ComponentType, rootId = 'root'): void {
const rootElement = document.getElementById(rootId);
@@ -17,6 +16,7 @@ export default function createView (Entry: React.ComponentType, rootId = 'root')
ReactDOM.render(
<Suspense fallback='...'>
<Fonts />
<View>
<HashRouter>
<Entry />
+19
View File
@@ -0,0 +1,19 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { useEffect, RefObject } from 'react';
export const useOutsideClick = (ref: RefObject<HTMLDivElement>, callback: () => void): void => {
const handleClick = (e: MouseEvent): void => {
if (ref.current && !ref.current.contains(e.target as HTMLInputElement)) {
callback();
}
};
useEffect(() => {
document.addEventListener('click', handleClick);
return (): void => {
document.removeEventListener('click', handleClick);
};
});
};
+30 -5
View File
@@ -4,8 +4,8 @@
import React from 'react';
import styled from 'styled-components';
import { Link } from '../components';
import ArrowLeftImage from '../assets/arrowLeft.svg';
import { Link, Svg } from '../components';
interface Props {
className?: string;
@@ -15,11 +15,36 @@ interface Props {
function Back ({ className, to = '/' }: Props): React.ReactElement<Props> {
return (
<div className={className}>
<Link to={to}>Back</Link>
<BackLink to={to}>
<ArrowLeft />
Back
</BackLink>
</div>
);
}
export default styled(Back)`
padding: 0 0.5rem;
const ArrowLeft = styled(Svg).attrs(() => ({
src: ArrowLeftImage
}))`
width: 12px;
height: 12px;
margin-right: 13px;
background: ${({ theme }): string => theme.labelColor};
`;
const BackLink = styled(Link)`
width: min-content;
text-decoration: underline;
color: ${({ theme }): string => theme.labelColor};
&:visited {
color: ${({ theme }): string => theme.labelColor};
}
`;
export default styled(Back)`
margin: 0;
line-height: 52px;
border-bottom: 1px solid ${({ theme }): string => theme.inputBorderColor};
font-size: ${({ theme }): string => theme.labelFontSize};
`;
+6 -5
View File
@@ -4,21 +4,21 @@
import React, { useContext, useEffect, useState } from 'react';
import { AccountContext, Input } from '../components';
import { AccountContext, InputWithLabel } from '../components';
interface Props {
address?: string;
className?: string;
defaultValue?: string | null;
isFocussed?: boolean;
label?: string | null;
label?: string;
onBlur?: () => void;
onChange: (name: string | null) => void;
}
const MIN_LENGTH = 3;
export default function Name ({ address, className, defaultValue, isFocussed, label = 'a descriptive name for this account', onBlur, onChange }: Props): React.ReactElement<Props> {
export default function Name ({ address, className, defaultValue, isFocussed, label = 'A descriptive name for your account', onBlur, onChange }: Props): React.ReactElement<Props> {
const accounts = useContext(AccountContext);
const [name, setName] = useState('');
const account = accounts.find((account): boolean => account.address === address);
@@ -36,11 +36,12 @@ export default function Name ({ address, className, defaultValue, isFocussed, la
}, [name]);
return (
<Input
<InputWithLabel
data-input-name
className={className}
defaultValue={startValue}
isError={isError}
isFocussed={isFocussed}
isFocused={isFocussed}
label={label}
onBlur={onBlur}
onChange={setName}
@@ -4,7 +4,7 @@
import React, { useEffect, useState } from 'react';
import { Input } from '../components';
import { InputWithLabel } from '../components';
interface Props {
isFocussed?: boolean;
@@ -27,17 +27,19 @@ export default function Password ({ isFocussed, onChange }: Props): React.ReactE
return (
<>
<Input
<InputWithLabel
data-input-password
isError={pass1.length < MIN_LENGTH}
isFocussed={isFocussed}
label='a new password for this account'
isFocused={isFocussed}
label='A new password for this account'
onChange={setPass1}
type='password'
/>
{(pass1.length >= MIN_LENGTH) && (
<Input
<InputWithLabel
data-input-repeat-password
isError={pass1 !== pass2}
label='repeat password for verification'
label='Repeat password for verification'
onChange={setPass2}
type='password'
/>
+5
View File
@@ -0,0 +1,5 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
export const flushAllPromises = (): Promise<void> => new Promise(resolve => setImmediate(resolve));
+2 -2
View File
@@ -4,8 +4,8 @@
<meta charset="utf-8">
<title>polkadot{.js}</title>
</head>
<body style="background:#f5f6f7; height:580px; margin:0; overflow-x:hidden; text-align:center">
<div id="root" style="box-sizing:border-box; margin:0 auto; padding:0 1rem; text-align:left; width:480px"></div>
<body style="height: 600px; margin:0; overflow-x:hidden; text-align:center">
<div id="root" style="box-sizing:border-box; margin:0 auto; padding:0; text-align:left; width:480px; height: calc(100% - 2px); max-width: 100%;"></div>
<script src='./extension.js'></script>
</body>
</html>
+1 -1
View File
@@ -57,7 +57,7 @@ function createWebpack ({ alias = {}, context }) {
]
},
{
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
test: [/\.svg$/, /\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/, /\.woff2?$/],
use: [
{
loader: require.resolve('url-loader'),