Swap to using context for accounts & requests (#6)

* Swap to using context for accounts & requests

* Move event handlers
This commit is contained in:
Jaco Greeff
2019-05-20 18:02:20 +02:00
committed by GitHub
parent e730d4e7c0
commit 8c25cb3e39
13 changed files with 363 additions and 324 deletions
@@ -6,55 +6,61 @@ import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import { ActionBar, Address } from '../../components';
import { ActionContext } from '../../components/contexts';
import { editAccount } from '../../messaging';
import { Name } from '../../partials';
type Props = {
className?: string,
address: string,
name?: string | null,
onAction: () => void
address: string
};
export default function Account ({ address, className, name, onAction }: Props) {
export default function Account ({ address, className }: Props) {
const [isEditing, setEditing] = useState(false);
const [editedname, setName] = useState(name);
const [editedname, setName] = useState(null as string | null);
const _toggleEdit = (): void => {
const toggleEdit = (): void => {
setEditing(!isEditing);
};
const _saveChanges = (): void => {
if (editedname && editedname !== name) {
editAccount(address, editedname)
.then(onAction)
.catch(console.error);
}
_toggleEdit();
};
return (
<Address
address={address}
className={className}
name={
isEditing
? (
<Name
defaultValue={name}
isFocussed
label={null}
onBlur={_saveChanges}
onChange={setName}
/>
)
: name
}
>
<ActionBar>
<a onClick={_toggleEdit}>Edit</a>
<Link to={`/account/forget/${address}`}>Forget</Link>
</ActionBar>
</Address>
<ActionContext.Consumer>
{(onAction) => {
const saveChanges = (): void => {
if (editedname && editedname !== name) {
editAccount(address, editedname)
.then(() => onAction())
.catch(console.error);
}
toggleEdit();
};
return (
<Address
address={address}
className={className}
name={
isEditing
? (
<Name
address={address}
isFocussed
label={null}
onBlur={saveChanges}
onChange={setName}
/>
)
: undefined
}
>
<ActionBar>
<a onClick={toggleEdit}>Edit</a>
<Link to={`/account/forget/${address}`}>Forget</Link>
</ActionBar>
</Address>
);
}}
</ActionContext.Consumer>
);
}
@@ -2,44 +2,40 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { KeyringJson } from '@polkadot/ui-keyring/types';
import React from 'react';
import { Button, Header, Tip } from '../../components';
import { AccountContext } from '../../components/contexts';
import Account from './Account';
type Props = {
accounts: Array<KeyringJson>,
onAction: () => void
};
type Props = {};
export default function Accounts ({ accounts, onAction }: Props) {
export default function Accounts (props: Props) {
return (
<div>
<Header label='accounts' />
{
(accounts.length === 0)
? <Tip header='add accounts' type='warn'>You currently don'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(({ address, meta: { name } }) => (
<Account
address={address}
key={address}
name={name}
onAction={onAction}
/>
))
}
<Button
isFull
label='I want to create a new account with a new seed'
to='/account/create'
/>
<Button
isFull
label='I have a pre-existing seed, import the account'
to='/account/import'
/>
</div>
<AccountContext.Consumer>
{(accounts) => (
<div>
<Header label='accounts' />
{
(accounts.length === 0)
? <Tip header='add accounts' type='warn'>You currently don'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(({ address }) => (
<Account
address={address}
key={address}
/>
))
}
<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'
/>
</div>
)}
</AccountContext.Consumer>
);
}
+49 -48
View File
@@ -5,14 +5,13 @@
import React, { useState, useEffect } from 'react';
import { Address, Button, Header, Loading, TextArea } from '../components';
import { ActionContext } from '../components/contexts';
import { createAccount, createSeed } from '../messaging';
import { Back, Name, Password } from '../partials';
type Props = {
onAction: () => void
};
type Props = {};
export default function Create ({ onAction }: Props) {
export default function Create (props: Props) {
const [account, setAccount] = useState(null as null | { address: string, seed: string });
const [name, setName] = useState(null as string | null);
const [password, setPassword] = useState(null as string | null);
@@ -23,50 +22,52 @@ export default function Create ({ onAction }: Props) {
.catch(console.error);
}, []);
// FIXME Duplicated between here and Import.tsx
const _onCreate = (): void => {
// this should always be the case
if (name && password && account) {
createAccount(name, password, account.seed)
.then(() => {
onAction();
window.location.hash = '/';
})
.catch(console.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
isFull
label='Add the account with the generated seed'
onClick={_onCreate}
/>
</>
)}
</>
)}</Loading>
</div>
<ActionContext.Consumer>
{(onAction) => {
// FIXME Duplicated between here and Import.tsx
const onCreate = (): void => {
// this should always be the case
if (name && password && account) {
createAccount(name, password, account.seed)
.then(() => onAction('/'))
.catch(console.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}
/>
</>
)}
</>
)}</Loading>
</div>
);
}}
</ActionContext.Consumer>
);
}
+25 -31
View File
@@ -2,48 +2,42 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { KeyringJson } from '@polkadot/ui-keyring/types';
import React from 'react';
import { RouteComponentProps, withRouter } from 'react-router';
import { Address, Button, Header, Tip } from '../components';
import { forgetAccount } from '../messaging';
import { Back } from '../partials';
import { ActionContext } from '../components/contexts';
type Props = RouteComponentProps<{ address: string }> & {
accounts: Array<KeyringJson>,
onAction: () => void
};
type Props = RouteComponentProps<{ address: string }> & {};
function Forget (props: Props) {
const { accounts, match: { params: { address } }, onAction } = props;
const account = accounts.find((account) => address === account.address);
const _onClick = (): void => {
forgetAccount(address)
.then(() => {
onAction();
window.location.hash = '/';
})
.catch(console.error);
};
const { match: { params: { address } } } = props;
return (
<div>
<Header label='forget account' />
<Back />
<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>
<Address
address={address}
name={account && account.meta.name}
/>
<Button
isFull
label='I want to forget this account'
onClick={_onClick}
/>
</div>
<ActionContext.Consumer>
{(onAction) => {
const onClick = (): void => {
forgetAccount(address)
.then(() => onAction('/'))
.catch(console.error);
};
return (
<div>
<Header label='forget account' />
<Back />
<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>
<Address address={address} />
<Button
label='I want to forget this account'
onClick={onClick}
/>
</div>
);
}}
</ActionContext.Consumer>
);
}
+43 -43
View File
@@ -5,19 +5,18 @@
import React, { useState } from 'react';
import { Address, Button, Header, TextArea } from '../components';
import { ActionContext } from '../components/contexts';
import { createAccount, validateSeed } from '../messaging';
import { Back, Name, Password } from '../partials';
type Props = {
onAction: () => void
};
type Props = {};
export default function Import ({ onAction }: Props) {
export default function Import (props: Props) {
const [account, setAccount] = useState(null as null | { address: string, seed: string });
const [name, setName] = useState(null as string | null);
const [password, setPassword] = useState(null as string | null);
const _onChangeSeed = (seed: string): void => {
const onChangeSeed = (seed: string): void => {
validateSeed(seed)
.then(setAccount)
.catch((error) => {
@@ -27,45 +26,46 @@ export default function Import ({ onAction }: Props) {
});
};
// FIXME Duplicated between here and Create.tsx
const _onCreate = (): void => {
// this should always be the case
if (name && password && account) {
createAccount(name, password, account.seed)
.then(() => {
onAction();
window.location.hash = '/';
})
.catch(console.error);
}
};
return (
<div>
<Header label='import account' />
<Back />
<TextArea
isError={!account}
isFocussed
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
isFull
label='Add the account with the supplied seed'
onClick={_onCreate}
/>
</>
)}
<ActionContext.Consumer>
{(onAction) => {
// FIXME Duplicated between here and Create.tsx
const onCreate = (): void => {
// this should always be the case
if (name && password && account) {
createAccount(name, password, account.seed)
.then(() => onAction('/'))
.catch(console.error);
}
};
</div>
return (
<div>
<Header label='import account' />
<Back />
<TextArea
isError={!account}
isFocussed
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}
/>
</>
)}
</div>
);
}}
</ActionContext.Consumer>
);
}
@@ -3,52 +3,50 @@
// of the Apache-2.0 license. See the LICENSE file for details.
import { MessageExtrinsicSign } from '@polkadot/extension/background/types';
import { KeyringJson } from '@polkadot/ui-keyring/types';
import React from 'react';
import { ActionBar, Address } from '../../components';
import { ActionContext } from '../../components/contexts';
import { cancelRequest } from '../../messaging';
import Details from './Details';
import Unlock from './Unlock';
type Props = {
accounts: Array<KeyringJson>,
className?: string,
isFirst: boolean,
onAction: () => void,
request: MessageExtrinsicSign,
signId: number,
url: string
};
export default function Request ({ accounts, isFirst, onAction, request: { address, method, nonce }, signId, url }: Props) {
const account = accounts.find((account) => address === account.address);
const _onCancel = (): void => {
cancelRequest(signId)
.then(onAction)
.catch(console.error);
};
export default function Request ({ isFirst, request: { address, method, nonce }, signId, url }: Props) {
return (
<Address
address={address}
name={account && account.meta.name}
>
<Details
method={method}
nonce={nonce}
url={url}
/>
<ActionBar>
<a onClick={_onCancel}>Cancel</a>
</ActionBar>
<Unlock
isVisible={isFirst}
onAction={onAction}
signId={signId}
/>
</Address>
<ActionContext.Consumer>
{(onAction) => {
const onCancel = (): void => {
cancelRequest(signId)
.then(() => onAction())
.catch(console.error);
};
return (
<Address address={address}>
<Details
method={method}
nonce={nonce}
url={url}
/>
<ActionBar>
<a onClick={onCancel}>Cancel</a>
</ActionBar>
<Unlock
isVisible={isFirst}
signId={signId}
/>
</Address>
);
}}
</ActionContext.Consumer>
);
}
@@ -5,15 +5,15 @@
import React, { useState, useEffect } from 'react';
import { Button, Input } from '../../components';
import { ActionContext } from '../../components/contexts';
import { approveRequest } from '../../messaging';
type Props = {
signId: number,
isVisible: boolean,
onAction: () => void
isVisible: boolean
};
export default function Unlock ({ isVisible, onAction, signId }: Props) {
export default function Unlock ({ isVisible, signId }: Props) {
const [error, setError] = useState('');
const [password, setPassword] = useState('');
@@ -27,29 +27,34 @@ export default function Unlock ({ isVisible, onAction, signId }: Props) {
}
}, [password]);
const _onSign = (): void => {
approveRequest(signId, password)
.then(onAction)
.catch((error) => {
console.error(error);
setError(error.message);
});
};
return (
<>
<Input
isError={!password || !!error}
isFocussed
label='password for this account'
onChange={setPassword}
type='password'
/>
<Button
isFull
label='Sign the extrinsic'
onClick={_onSign}
/>
</>
<ActionContext.Consumer>
{(onAction) => {
const onSign = (): void => {
approveRequest(signId, password)
.then(() => onAction())
.catch((error) => {
console.error(error);
setError(error.message);
});
};
return (
<>
<Input
isError={!password || !!error}
isFocussed
label='password for this account'
onChange={setPassword}
type='password'
/>
<Button
label='Sign the extrinsic'
onClick={onSign}
/>
</>
);
}}
</ActionContext.Consumer>
);
}
@@ -2,35 +2,31 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { MessageExtrinsicSign } from '@polkadot/extension/background/types';
import { KeyringJson } from '@polkadot/ui-keyring/types';
import React from 'react';
import { Header } from '../../components';
import { SigningContext } from '../../components/contexts';
import Request from './Request';
type Props = {
accounts: Array<KeyringJson>,
requests: Array<[number, MessageExtrinsicSign, string]>,
onAction: () => void
};
type Props = {};
export default function Signing ({ accounts, onAction, requests }: Props) {
export default function Signing (props: Props) {
return (
<div>
<Header label='requests' />
{requests.map(([id, request, url], index) => (
<Request
accounts={accounts}
isFirst={index === 0}
key={id}
onAction={onAction}
request={request}
signId={id}
url={url}
/>
))}
</div>
<SigningContext.Consumer>
{(requests) => (
<div>
<Header label='requests' />
{requests.map(([id, request, url], index) => (
<Request
isFirst={index === 0}
key={id}
request={request}
signId={id}
url={url}
/>
))}
</div>
)}
</SigningContext.Consumer>
);
}
+18 -18
View File
@@ -9,6 +9,7 @@ import React, { useEffect, useState } from 'react';
import { Route, Switch } from 'react-router';
import { Loading } from '../components';
import { AccountContext, ActionContext, SigningContext } from '../components/contexts';
import { getAccounts, getRequests } from '../messaging';
import Accounts from './Accounts';
import Create from './Create';
@@ -22,7 +23,7 @@ export default function Popup (props: Props) {
const [accounts, setAccounts] = useState(null as null | Array<KeyringJson>);
const [requests, setRequests] = useState(null as null | Array<[number, MessageExtrinsicSign, string]>);
const onAction = (): void => {
const onAction = (to?: string): void => {
// loads all accounts & requests (this is passed through to children to trigger changes)
Promise
.all([getAccounts(), getRequests()])
@@ -31,6 +32,10 @@ export default function Popup (props: Props) {
setRequests(requests);
})
.catch(console.error);
if (to) {
window.location.hash = to;
}
};
useEffect((): void => {
@@ -40,23 +45,18 @@ export default function Popup (props: Props) {
return (
<Loading>{accounts && requests && (
<Switch>
<Route path='/account/create' component={() =>
<Create onAction={onAction} />
} />
<Route path='/account/forget/:address' component={() =>
<Forget accounts={accounts} onAction={onAction} />
} />
<Route path='/account/import' component={() =>
<Import onAction={onAction} />
} />
<Route
exact path='/' component={() =>
requests.length
? <Signing accounts={accounts} onAction={onAction} requests={requests} />
: <Accounts accounts={accounts} onAction={onAction} />
} />
</Switch>
<ActionContext.Provider value={onAction}>
<AccountContext.Provider value={accounts}>
<SigningContext.Provider value={requests}>
<Switch>
<Route path='/account/create' component={Create} />
<Route path='/account/forget/:address' component={Forget} />
<Route path='/account/import' component={Import} />
<Route exact path='/' component={requests.length ? Signing : Accounts} />
</Switch>
</SigningContext.Provider>
</AccountContext.Provider>
</ActionContext.Provider>
)}</Loading>
);
}
@@ -7,6 +7,7 @@ import styled from 'styled-components';
import Identicon from '@polkadot/ui-identicon';
import Box from './Box';
import { AccountContext } from './contexts';
import defaults from './defaults';
type Props = {
@@ -24,19 +25,27 @@ function Address ({ address, children, className, isHidden, name, theme = 'polka
}
return (
<Box className={className}>
<div>
<div className='name'>{name || '<unknown>'}</div>
<div className='address'>{address || '<unknown>'}</div>
<div className='children'>{children}</div>
<Identicon
className='icon'
size={64}
theme={theme}
value={address}
/>
</div>
</Box>
<AccountContext.Consumer>
{(accounts) => {
const account = accounts.find((account) => account.address === address);
return (
<Box className={className}>
<div>
<div className='name'>{name || (account && account.meta.name) || '<unknown>'}</div>
<div className='address'>{address || '<unknown>'}</div>
<div className='children'>{children}</div>
<Identicon
className='icon'
size={64}
theme={theme}
value={address}
/>
</div>
</Box>
);
}}
</AccountContext.Consumer>
);
}
@@ -11,7 +11,7 @@ type Props = {
className?: string;
children?: React.ReactNode,
isDisabled?: boolean,
isFull?: boolean,
isSmall?: boolean,
label?: string,
onClick?: () => void,
to?: string
@@ -43,17 +43,17 @@ function Button ({ children, className, isDisabled, label, onClick, to }: Props)
export default styled(Button)`
box-sizing: border-box;
display: ${({ isFull }) =>
isFull
? 'block'
: 'inline-block'
display: ${({ isSmall }) =>
isSmall
? 'inline-block'
: 'block'
};
margin: ${defaults.boxMargin};
padding: ${defaults.boxPadding};
width: ${({ isFull }) =>
isFull
? '100%'
: 'auto'
width: ${({ isSmall }) =>
isSmall
? 'auto'
: '100%'
};
button {
@@ -0,0 +1,22 @@
// 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 { MessageExtrinsicSign } from '@polkadot/extension/background/types';
import { KeyringJson } from '@polkadot/ui-keyring/types';
import React from 'react';
const noop = (to?: string) => {
// do nothing
};
const AccountContext = React.createContext([] as Array<KeyringJson>);
const ActionContext = React.createContext(noop);
const SigningContext = React.createContext([] as Array<[number, MessageExtrinsicSign, string]>);
export {
AccountContext,
ActionContext,
SigningContext
};
+26 -14
View File
@@ -5,20 +5,21 @@
import React, { useEffect, useState } from 'react';
import { Input } from '../components';
import { AccountContext } from '../components/contexts';
type Props = {
address?: string,
defaultValue?: string | null,
isFocussed?: boolean,
label?: string | null,
onBlur?: () => void,
onChange: (name: string | null) => void,
value?: string
onChange: (name: string | null) => void
};
const MIN_LENGTH = 3;
export default function Name ({ defaultValue, isFocussed, label = 'a descriptive name for this account', onBlur, onChange, value }: Props) {
const [name, setName] = useState(defaultValue || '');
export default function Name ({ address, defaultValue, isFocussed, label = 'a descriptive name for this account', onBlur, onChange }: Props) {
const [name, setName] = useState('');
useEffect(() => {
onChange(
@@ -29,15 +30,26 @@ export default function Name ({ defaultValue, isFocussed, label = 'a descriptive
}, [name]);
return (
<Input
defaultValue={defaultValue}
isError={name.length < MIN_LENGTH}
isFocussed={isFocussed}
label={label}
onBlur={onBlur}
onChange={setName}
type='text'
value={value}
/>
<AccountContext.Consumer>
{(accounts) => {
const account = accounts.find((account) => account.address === address);
const startValue = (account && account.meta.name) || defaultValue;
const isError = !name && startValue
? false
: (name.length < MIN_LENGTH);
return (
<Input
defaultValue={startValue}
isError={isError}
isFocussed={isFocussed}
label={label}
onBlur={onBlur}
onChange={setName}
type='text'
/>
);
}}
</AccountContext.Consumer>
);
}