Account/Address create modals (#1221)
* Account/Address create modals * Spacing * Account Create modal (& use by vanity) * Account Create & Import wired in
This commit is contained in:
parent
23c62c6a1e
commit
f76f1ab594
@ -2,20 +2,30 @@
|
||||
// This software may be modified and distributed under the terms
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { ComponentProps } from './types';
|
||||
import { I18nProps } from '@polkadot/ui-app/types';
|
||||
import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
|
||||
import { ComponentProps } from './types';
|
||||
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import accountObservable from '@polkadot/ui-keyring/observable/accounts';
|
||||
import { withMulti, withObservable } from '@polkadot/ui-api';
|
||||
import { Button } from '@polkadot/ui-app';
|
||||
|
||||
import CreateModal from './modals/Create';
|
||||
import ImportModal from './modals/Import';
|
||||
import Account from './Account';
|
||||
import translate from './translate';
|
||||
|
||||
type Props = ComponentProps & {
|
||||
type Props = ComponentProps & I18nProps & {
|
||||
accounts?: SubjectInfo[]
|
||||
};
|
||||
|
||||
type State = {
|
||||
isCreateOpen: boolean,
|
||||
isImportOpen: boolean
|
||||
};
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.accounts {
|
||||
display: flex;
|
||||
@ -29,37 +39,71 @@ const Wrapper = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
class Overview extends React.PureComponent<Props> {
|
||||
constructor (props: Props) {
|
||||
super(props);
|
||||
}
|
||||
class Overview extends React.PureComponent<Props, State> {
|
||||
state: State = {
|
||||
isCreateOpen: false,
|
||||
isImportOpen: false
|
||||
};
|
||||
|
||||
render () {
|
||||
const { accounts } = this.props;
|
||||
|
||||
if (!accounts || Object.keys(accounts).length === 0) {
|
||||
return null;
|
||||
}
|
||||
const { accounts, onStatusChange, t } = this.props;
|
||||
const { isCreateOpen, isImportOpen } = this.state;
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Button.Group>
|
||||
<Button
|
||||
isPrimary
|
||||
label={t('Add account')}
|
||||
onClick={this.toggleCreate}
|
||||
/>
|
||||
<Button.Or />
|
||||
<Button
|
||||
isPrimary
|
||||
label={t('Restore JSON')}
|
||||
onClick={this.toggleImport}
|
||||
/>
|
||||
</Button.Group>
|
||||
{isCreateOpen && (
|
||||
<CreateModal
|
||||
onClose={this.toggleCreate}
|
||||
onStatusChange={onStatusChange}
|
||||
/>
|
||||
)}
|
||||
{isImportOpen && (
|
||||
<ImportModal
|
||||
onClose={this.toggleImport}
|
||||
onStatusChange={onStatusChange}
|
||||
/>
|
||||
)}
|
||||
<div className='accounts'>
|
||||
{Object.keys(accounts).map((address) => {
|
||||
return (
|
||||
<Account
|
||||
address={address}
|
||||
key={address}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{accounts && Object.keys(accounts).map((address) => (
|
||||
<Account
|
||||
address={address}
|
||||
key={address}
|
||||
/>
|
||||
))}
|
||||
<div className='spacer' />
|
||||
</div>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
private toggleCreate = (): void => {
|
||||
this.setState(({ isCreateOpen }) => ({
|
||||
isCreateOpen: !isCreateOpen
|
||||
}));
|
||||
}
|
||||
|
||||
private toggleImport = (): void => {
|
||||
this.setState(({ isImportOpen }) => ({
|
||||
isImportOpen: !isImportOpen
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
export default withMulti(
|
||||
Overview,
|
||||
translate,
|
||||
withObservable(accountObservable.subject, { propName: 'accounts' })
|
||||
);
|
||||
|
||||
@ -12,6 +12,7 @@ import styled from 'styled-components';
|
||||
import { Button, Dropdown, Input, TxComponent } from '@polkadot/ui-app';
|
||||
import uiSettings from '@polkadot/ui-settings';
|
||||
|
||||
import CreateModal from '../modals/Create';
|
||||
import generator from '../vanitygen';
|
||||
import matchRegex from '../vanitygen/regex';
|
||||
import generatorSort from '../vanitygen/sort';
|
||||
@ -21,6 +22,7 @@ import translate from './translate';
|
||||
type Props = ComponentProps & I18nProps;
|
||||
|
||||
type State = {
|
||||
createSeed: string | null,
|
||||
elapsed: number,
|
||||
isMatchValid: boolean,
|
||||
isRunning: boolean,
|
||||
@ -54,6 +56,7 @@ const Wrapper = styled.div`
|
||||
class VanityApp extends TxComponent<Props, State> {
|
||||
results: Array<Generator$Result> = [];
|
||||
state: State = {
|
||||
createSeed: null,
|
||||
elapsed: 0,
|
||||
isMatchValid: true,
|
||||
isRunning: false,
|
||||
@ -73,12 +76,23 @@ class VanityApp extends TxComponent<Props, State> {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { onStatusChange } = this.props;
|
||||
const { createSeed, type } = this.state;
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
{this.renderOptions()}
|
||||
{this.renderButtons()}
|
||||
{this.renderStats()}
|
||||
{this.renderMatches()}
|
||||
{createSeed && (
|
||||
<CreateModal
|
||||
onClose={this.closeCreate}
|
||||
onStatusChange={onStatusChange}
|
||||
seed={createSeed}
|
||||
type={type}
|
||||
/>
|
||||
)}
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
@ -249,11 +263,8 @@ class VanityApp extends TxComponent<Props, State> {
|
||||
}, 0);
|
||||
}
|
||||
|
||||
private onCreateToggle = (seed: string) => {
|
||||
const { basePath } = this.props;
|
||||
const { type } = this.state;
|
||||
|
||||
window.location.hash = `${basePath}/create/${type}/${seed}`;
|
||||
private onCreateToggle = (createSeed: string) => {
|
||||
this.setState({ createSeed });
|
||||
}
|
||||
|
||||
private onChangeCase = (withCase: boolean): void => {
|
||||
@ -299,6 +310,10 @@ class VanityApp extends TxComponent<Props, State> {
|
||||
this.executeGeneration
|
||||
);
|
||||
}
|
||||
|
||||
private closeCreate = () => {
|
||||
this.setState({ createSeed: null });
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(VanityApp);
|
||||
|
||||
@ -16,9 +16,7 @@ import { withMulti, withObservable } from '@polkadot/ui-api';
|
||||
import './index.css';
|
||||
|
||||
import basicMd from './md/basic.md';
|
||||
import Creator from './Creator';
|
||||
import Overview from './Overview';
|
||||
import Restore from './Restore';
|
||||
import translate from './translate';
|
||||
import Vanity from './Vanity';
|
||||
|
||||
@ -49,15 +47,6 @@ class AccountsApp extends React.PureComponent<Props, State> {
|
||||
name: 'overview',
|
||||
text: t('Overview')
|
||||
},
|
||||
{
|
||||
hasParams: true,
|
||||
name: 'create',
|
||||
text: t('Create account')
|
||||
},
|
||||
{
|
||||
name: 'restore',
|
||||
text: t('Restore account')
|
||||
},
|
||||
{
|
||||
name: 'vanity',
|
||||
text: t('Vanity address')
|
||||
@ -77,7 +66,7 @@ class AccountsApp extends React.PureComponent<Props, State> {
|
||||
// Hide vanity as well - since the route order and matching changes, the
|
||||
// /create/:seed route become problematic, so don't allow that option
|
||||
return {
|
||||
hidden: ['overview','vanity']
|
||||
hidden: ['vanity']
|
||||
};
|
||||
}
|
||||
|
||||
@ -98,7 +87,6 @@ class AccountsApp extends React.PureComponent<Props, State> {
|
||||
render () {
|
||||
const { basePath } = this.props;
|
||||
const { hidden, tabs } = this.state;
|
||||
const renderCreator = this.renderComponent(Creator);
|
||||
|
||||
return (
|
||||
<main className='accounts--App'>
|
||||
@ -111,17 +99,8 @@ class AccountsApp extends React.PureComponent<Props, State> {
|
||||
/>
|
||||
</header>
|
||||
<Switch>
|
||||
<Route path={`${basePath}/create/:type/:seed`} render={renderCreator} />
|
||||
<Route path={`${basePath}/create`} render={renderCreator} />
|
||||
<Route path={`${basePath}/restore`} render={this.renderComponent(Restore)} />
|
||||
<Route path={`${basePath}/vanity`} render={this.renderComponent(Vanity)} />
|
||||
<Route
|
||||
render={
|
||||
hidden.includes('overview')
|
||||
? renderCreator
|
||||
: this.renderComponent(Overview)
|
||||
}
|
||||
/>
|
||||
<Route render={this.renderComponent(Overview)} />
|
||||
</Switch>
|
||||
</main>
|
||||
);
|
||||
|
||||
@ -6,28 +6,24 @@ import { I18nProps } from '@polkadot/ui-app/types';
|
||||
import { ApiProps } from '@polkadot/ui-api/types';
|
||||
import { ActionStatus } from '@polkadot/ui-app/Status/types';
|
||||
import { KeypairType } from '@polkadot/util-crypto/types';
|
||||
import { ComponentProps } from './types';
|
||||
import { ModalProps } from '../types';
|
||||
|
||||
import FileSaver from 'file-saver';
|
||||
import React from 'react';
|
||||
import { DEV_PHRASE } from '@polkadot/keyring/defaults';
|
||||
import { withApi, withMulti } from '@polkadot/ui-api';
|
||||
import { AddressRow, Button, Dropdown, Input, InputTags, Labelled, Modal, Password, TxComponent } from '@polkadot/ui-app';
|
||||
import { AddressRow, Button, Dropdown, Input, InputTags, Labelled, Modal, Password } from '@polkadot/ui-app';
|
||||
import { InputAddress } from '@polkadot/ui-app/InputAddress';
|
||||
import keyring from '@polkadot/ui-keyring';
|
||||
import uiSettings from '@polkadot/ui-settings';
|
||||
import { isHex, u8aToHex } from '@polkadot/util';
|
||||
import { keyExtractPath, mnemonicGenerate, mnemonicValidate, randomAsU8a } from '@polkadot/util-crypto';
|
||||
|
||||
import translate from './translate';
|
||||
import translate from '../translate';
|
||||
|
||||
type Props = ComponentProps & ApiProps & I18nProps & {
|
||||
match: {
|
||||
params: {
|
||||
seed?: string,
|
||||
type?: KeypairType
|
||||
}
|
||||
}
|
||||
type Props = ModalProps & ApiProps & I18nProps & {
|
||||
seed?: string,
|
||||
type?: KeypairType
|
||||
};
|
||||
|
||||
type SeedType = 'bip' | 'raw' | 'dev';
|
||||
@ -90,13 +86,13 @@ function addressFromSeed (phrase: string, derivePath: string, pairType: KeypairT
|
||||
.address();
|
||||
}
|
||||
|
||||
class Creator extends TxComponent<Props, State> {
|
||||
class Create extends React.PureComponent<Props, State> {
|
||||
state: State = { seedType: 'bip' } as State;
|
||||
|
||||
constructor (props: Props) {
|
||||
super(props);
|
||||
|
||||
const { isDevelopment, match: { params: { seed, type } }, t } = this.props;
|
||||
const { isDevelopment, seed, t, type } = this.props;
|
||||
const seedOptions: Array<SeedOption> = [
|
||||
{ value: 'bip', text: t('Mnemonic') },
|
||||
{ value: 'raw', text: t('Raw seed') }
|
||||
@ -113,24 +109,18 @@ class Creator extends TxComponent<Props, State> {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { address, name, isSeedValid } = this.state;
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<div className='accounts--Creator'>
|
||||
<Modal
|
||||
dimmer='inverted'
|
||||
open
|
||||
>
|
||||
<Modal.Header>{t('Add an account via seed')}</Modal.Header>
|
||||
{this.renderModal()}
|
||||
<div className='ui--grid'>
|
||||
{this.renderInput()}
|
||||
<AddressRow
|
||||
className='shrink'
|
||||
defaultName={name}
|
||||
value={
|
||||
isSeedValid
|
||||
? address
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{this.renderInput()}
|
||||
{this.renderButtons()}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@ -139,26 +129,27 @@ class Creator extends TxComponent<Props, State> {
|
||||
const { isValid } = this.state;
|
||||
|
||||
return (
|
||||
<Button.Group>
|
||||
<Button
|
||||
label={t('Reset')}
|
||||
onClick={this.onDiscard}
|
||||
/>
|
||||
<Button.Or />
|
||||
<Button
|
||||
isDisabled={!isValid}
|
||||
isPrimary
|
||||
label={t('Save')}
|
||||
onClick={this.onShowWarning}
|
||||
ref={this.button}
|
||||
/>
|
||||
</Button.Group>
|
||||
<Modal.Actions>
|
||||
<Button.Group>
|
||||
<Button
|
||||
label={t('Cancel')}
|
||||
onClick={this.onDiscard}
|
||||
/>
|
||||
<Button.Or />
|
||||
<Button
|
||||
isDisabled={!isValid}
|
||||
isPrimary
|
||||
label={t('Save')}
|
||||
onClick={this.onShowWarning}
|
||||
/>
|
||||
</Button.Group>
|
||||
</Modal.Actions>
|
||||
);
|
||||
}
|
||||
|
||||
private renderInput () {
|
||||
const { t } = this.props;
|
||||
const { deriveError, derivePath, isNameValid, isPassValid, isSeedValid, name, pairType, password, seed, seedOptions, seedType, tags } = this.state;
|
||||
const { address, deriveError, derivePath, isNameValid, isPassValid, isSeedValid, name, pairType, password, seed, seedOptions, seedType, tags } = this.state;
|
||||
const seedLabel = (() => {
|
||||
switch (seedType) {
|
||||
case 'bip':
|
||||
@ -171,8 +162,11 @@ class Creator extends TxComponent<Props, State> {
|
||||
})();
|
||||
|
||||
return (
|
||||
<div className='grow'>
|
||||
<div className='ui--row'>
|
||||
<Modal.Content>
|
||||
<AddressRow
|
||||
defaultName={name}
|
||||
value={isSeedValid ? address : ''}
|
||||
>
|
||||
<Input
|
||||
autoFocus
|
||||
className='full'
|
||||
@ -180,11 +174,9 @@ class Creator extends TxComponent<Props, State> {
|
||||
isError={!isNameValid}
|
||||
label={t('name')}
|
||||
onChange={this.onChangeName}
|
||||
onEnter={this.submit}
|
||||
onEnter={this.onCommit}
|
||||
value={name}
|
||||
/>
|
||||
</div>
|
||||
<div className='ui--row'>
|
||||
<Input
|
||||
className='full'
|
||||
help={t('The private key for your account is derived from this seed. This seed must be kept secret as anyone in its possession has access to the funds of this account. If you validate, use the seed of the session account as the "--key" parameter of your node.')}
|
||||
@ -192,7 +184,7 @@ class Creator extends TxComponent<Props, State> {
|
||||
isError={!isSeedValid}
|
||||
label={seedLabel}
|
||||
onChange={this.onChangeSeed}
|
||||
onEnter={this.submit}
|
||||
onEnter={this.onCommit}
|
||||
value={seed}
|
||||
>
|
||||
<Dropdown
|
||||
@ -202,61 +194,56 @@ class Creator extends TxComponent<Props, State> {
|
||||
options={seedOptions}
|
||||
/>
|
||||
</Input>
|
||||
</div>
|
||||
<div className='ui--row'>
|
||||
<Password
|
||||
className='full'
|
||||
help={t('This password is used to encrypt your private key. It must be strong and unique! You will need it to sign transactions with this account. You can recover this account using this password together with the backup file (generated in the next step).')}
|
||||
isError={!isPassValid}
|
||||
label={t('password')}
|
||||
onChange={this.onChangePass}
|
||||
onEnter={this.submit}
|
||||
onEnter={this.onCommit}
|
||||
value={password}
|
||||
/>
|
||||
</div>
|
||||
<div className='ui--row'>
|
||||
<InputTags
|
||||
help={t('Additional user-specified tags that can be used to identify the account. Tags can be used for categorization and filtering.')}
|
||||
label={t('user-defined tags')}
|
||||
onChange={this.onChangeTags}
|
||||
value={tags}
|
||||
/>
|
||||
</div>
|
||||
<details
|
||||
className='accounts--Creator-advanced'
|
||||
open
|
||||
>
|
||||
<summary>{t('Advanced creation options')}</summary>
|
||||
<div className='ui--Params'>
|
||||
<div className='ui--row'>
|
||||
<Dropdown
|
||||
defaultValue={pairType}
|
||||
help={t('Determines what cryptography will be used to create this account. Note that to validate on Polkadot, the session account must use "ed25519".')}
|
||||
label={t('keypair crypto type')}
|
||||
onChange={this.onChangePairType}
|
||||
options={uiSettings.availableCryptos}
|
||||
/>
|
||||
<details
|
||||
className='accounts--Creator-advanced'
|
||||
open
|
||||
>
|
||||
<summary>{t('Advanced creation options')}</summary>
|
||||
<div className='ui--Params'>
|
||||
<div className='ui--row'>
|
||||
<Dropdown
|
||||
defaultValue={pairType}
|
||||
help={t('Determines what cryptography will be used to create this account. Note that to validate on Polkadot, the session account must use "ed25519".')}
|
||||
label={t('keypair crypto type')}
|
||||
onChange={this.onChangePairType}
|
||||
options={uiSettings.availableCryptos}
|
||||
/>
|
||||
</div>
|
||||
<div className='ui--row'>
|
||||
<Input
|
||||
className='full'
|
||||
help={t('You can set a custom derivation path for this account using the following syntax "/<soft-key>//<hard-key>". The "/<soft-key>" and "//<hard-key>" may be repeated and mixed`.')}
|
||||
isError={!!deriveError}
|
||||
label={t('secret derivation path')}
|
||||
onChange={this.onChangeDerive}
|
||||
onEnter={this.onCommit}
|
||||
value={derivePath}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
deriveError
|
||||
? <Labelled label=''><article className='error'>{deriveError}</article></Labelled>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
<div className='ui--row'>
|
||||
<Input
|
||||
className='full'
|
||||
help={t('You can set a custom derivation path for this account using the following syntax "/<soft-key>//<hard-key>". The "/<soft-key>" and "//<hard-key>" may be repeated and mixed`.')}
|
||||
isError={!!deriveError}
|
||||
label={t('secret derivation path')}
|
||||
onChange={this.onChangeDerive}
|
||||
onEnter={this.submit}
|
||||
value={derivePath}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
deriveError
|
||||
? <Labelled label=''><article className='error'>{deriveError}</article></Labelled>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
</details>
|
||||
{this.renderButtons()}
|
||||
</div>
|
||||
</details>
|
||||
</AddressRow>
|
||||
</Modal.Content>
|
||||
);
|
||||
}
|
||||
|
||||
@ -266,7 +253,6 @@ class Creator extends TxComponent<Props, State> {
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className='app--accounts-Modal'
|
||||
dimmer='inverted'
|
||||
open={showWarning}
|
||||
>
|
||||
@ -417,12 +403,13 @@ class Creator extends TxComponent<Props, State> {
|
||||
}
|
||||
|
||||
private onCommit = (): void => {
|
||||
const { basePath, onStatusChange, t } = this.props;
|
||||
const { derivePath, name, pairType, password, seed, tags } = this.state;
|
||||
const { onClose, onStatusChange, t } = this.props;
|
||||
const { derivePath, isValid, name, pairType, password, seed, tags } = this.state;
|
||||
const status = { action: 'create' } as ActionStatus;
|
||||
|
||||
const status = {
|
||||
action: 'create'
|
||||
} as ActionStatus;
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { json, pair } = keyring.addUri(`${seed}${derivePath}`, password, { name, tags }, pairType);
|
||||
@ -443,14 +430,13 @@ class Creator extends TxComponent<Props, State> {
|
||||
this.onHideWarning();
|
||||
|
||||
onStatusChange(status);
|
||||
|
||||
window.location.hash = basePath;
|
||||
onClose();
|
||||
}
|
||||
|
||||
private onDiscard = (): void => {
|
||||
this.setState(({ pairType }) =>
|
||||
this.emptyState(null, '', pairType)
|
||||
);
|
||||
const { onClose } = this.props;
|
||||
|
||||
onClose();
|
||||
}
|
||||
|
||||
private selectSeedType = (seedType: SeedType): void => {
|
||||
@ -466,7 +452,7 @@ class Creator extends TxComponent<Props, State> {
|
||||
}
|
||||
|
||||
export default withMulti(
|
||||
Creator,
|
||||
Create,
|
||||
translate,
|
||||
withApi
|
||||
);
|
||||
@ -5,17 +5,17 @@
|
||||
import { KeyringPair$Json } from '@polkadot/keyring/types';
|
||||
import { I18nProps } from '@polkadot/ui-app/types';
|
||||
import { ActionStatus } from '@polkadot/ui-app/Status/types';
|
||||
import { ComponentProps } from './types';
|
||||
import { ModalProps } from '../types';
|
||||
|
||||
import React from 'react';
|
||||
import { AddressRow, Button, InputFile, Password, TxComponent } from '@polkadot/ui-app';
|
||||
import { AddressRow, Button, InputFile, Modal, Password, TxComponent } from '@polkadot/ui-app';
|
||||
import { InputAddress } from '@polkadot/ui-app/InputAddress';
|
||||
import { isHex, isObject, u8aToString } from '@polkadot/util';
|
||||
import keyring from '@polkadot/ui-keyring';
|
||||
|
||||
import translate from './translate';
|
||||
import translate from '../translate';
|
||||
|
||||
type Props = ComponentProps & I18nProps;
|
||||
type Props = ModalProps & I18nProps;
|
||||
|
||||
type State = {
|
||||
address: string | null,
|
||||
@ -25,7 +25,7 @@ type State = {
|
||||
password: string
|
||||
};
|
||||
|
||||
class Restore extends TxComponent<Props, State> {
|
||||
class Import extends TxComponent<Props, State> {
|
||||
state: State = {
|
||||
address: null,
|
||||
isFileValid: false,
|
||||
@ -35,33 +35,47 @@ class Restore extends TxComponent<Props, State> {
|
||||
};
|
||||
|
||||
render () {
|
||||
const { address, isFileValid } = this.state;
|
||||
const { onClose, t } = this.props;
|
||||
const { isFileValid, isPassValid } = this.state;
|
||||
|
||||
return (
|
||||
<div className='accounts--Restore'>
|
||||
<div className='ui--grid'>
|
||||
{this.renderInput()}
|
||||
<AddressRow
|
||||
className='shrink'
|
||||
value={
|
||||
isFileValid && address
|
||||
? address
|
||||
: null
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Modal
|
||||
dimmer='inverted'
|
||||
open
|
||||
>
|
||||
<Modal.Header>{t('Add an account via seed')}</Modal.Header>
|
||||
{this.renderInput()}
|
||||
<Modal.Actions>
|
||||
<Button.Group>
|
||||
<Button
|
||||
label={t('Cancel')}
|
||||
onClick={onClose}
|
||||
/>
|
||||
<Button.Or />
|
||||
<Button
|
||||
isDisabled={!isFileValid || !isPassValid}
|
||||
isPrimary
|
||||
onClick={this.onSave}
|
||||
label={t('Restore')}
|
||||
ref={this.button}
|
||||
/>
|
||||
</Button.Group>
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
private renderInput () {
|
||||
const { t } = this.props;
|
||||
const { isFileValid, isPassValid, password } = this.state;
|
||||
const { address, isFileValid, isPassValid, json, password } = this.state;
|
||||
const acceptedFormats = ['application/json', 'text/plain'].join(', ');
|
||||
|
||||
return (
|
||||
<div className='grow'>
|
||||
<div className='ui--row'>
|
||||
<Modal.Content>
|
||||
<AddressRow
|
||||
defaultName={isFileValid && json ? json.meta.name : null}
|
||||
value={isFileValid && address ? address : null}
|
||||
>
|
||||
<InputFile
|
||||
accept={acceptedFormats}
|
||||
className='full'
|
||||
@ -71,8 +85,6 @@ class Restore extends TxComponent<Props, State> {
|
||||
onChange={this.onChangeFile}
|
||||
withLabel
|
||||
/>
|
||||
</div>
|
||||
<div className='ui--row'>
|
||||
<Password
|
||||
autoFocus
|
||||
className='full'
|
||||
@ -83,17 +95,8 @@ class Restore extends TxComponent<Props, State> {
|
||||
onEnter={this.submit}
|
||||
value={password}
|
||||
/>
|
||||
</div>
|
||||
<Button.Group>
|
||||
<Button
|
||||
isDisabled={!isFileValid || !isPassValid}
|
||||
isPrimary
|
||||
onClick={this.onSave}
|
||||
label={t('Restore')}
|
||||
ref={this.button}
|
||||
/>
|
||||
</Button.Group>
|
||||
</div>
|
||||
</AddressRow>
|
||||
</Modal.Content>
|
||||
);
|
||||
}
|
||||
|
||||
@ -131,16 +134,14 @@ class Restore extends TxComponent<Props, State> {
|
||||
}
|
||||
|
||||
private onSave = (): void => {
|
||||
const { basePath, onStatusChange, t } = this.props;
|
||||
const { onClose, onStatusChange, t } = this.props;
|
||||
const { json, password } = this.state;
|
||||
|
||||
if (!json) {
|
||||
return;
|
||||
}
|
||||
|
||||
const status = {
|
||||
action: 'restore'
|
||||
} as ActionStatus;
|
||||
const status = { action: 'restore' } as ActionStatus;
|
||||
|
||||
try {
|
||||
const pair = keyring.restoreAccount(json, password);
|
||||
@ -161,9 +162,9 @@ class Restore extends TxComponent<Props, State> {
|
||||
onStatusChange(status);
|
||||
|
||||
if (status.status !== 'error') {
|
||||
window.location.hash = basePath;
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(Restore);
|
||||
export default translate(Import);
|
||||
@ -3,6 +3,7 @@
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { AppProps } from '@polkadot/ui-app/types';
|
||||
import { ActionStatus } from '@polkadot/ui-app/Status/types';
|
||||
|
||||
import { WithTranslation } from 'react-i18next';
|
||||
|
||||
@ -22,3 +23,8 @@ export type BareProps = {
|
||||
export type I18nProps = BareProps & WithTranslation;
|
||||
|
||||
export type ComponentProps = AppProps & LocationProps;
|
||||
|
||||
export type ModalProps = {
|
||||
onClose: () => void,
|
||||
onStatusChange: (status: ActionStatus) => void
|
||||
};
|
||||
|
||||
@ -16,6 +16,7 @@ import Tabs, { TabItem } from '@polkadot/ui-app/Tabs';
|
||||
import { withMulti, withObservable } from '@polkadot/ui-api';
|
||||
|
||||
import basicMd from './md/basic.md';
|
||||
import CreateModal from './modals/Create';
|
||||
import Creator from './Creator';
|
||||
import Editor from './Editor';
|
||||
import translate from './translate';
|
||||
@ -26,7 +27,8 @@ type Props = AppProps & I18nProps & {
|
||||
|
||||
type State = {
|
||||
hidden: Array<string>,
|
||||
items: Array<TabItem>
|
||||
items: Array<TabItem>,
|
||||
isCreateOpen: boolean
|
||||
};
|
||||
|
||||
class AddressBookApp extends React.PureComponent<Props, State> {
|
||||
@ -42,6 +44,7 @@ class AddressBookApp extends React.PureComponent<Props, State> {
|
||||
|
||||
this.state = {
|
||||
...baseState,
|
||||
isCreateOpen: false,
|
||||
items: [
|
||||
{
|
||||
name: 'edit',
|
||||
@ -82,8 +85,8 @@ class AddressBookApp extends React.PureComponent<Props, State> {
|
||||
}
|
||||
|
||||
render () {
|
||||
const { basePath } = this.props;
|
||||
const { hidden, items } = this.state;
|
||||
const { basePath, onStatusChange } = this.props;
|
||||
const { hidden, isCreateOpen, items } = this.state;
|
||||
const renderCreator = this.renderComponent(Creator);
|
||||
|
||||
return (
|
||||
@ -106,6 +109,12 @@ class AddressBookApp extends React.PureComponent<Props, State> {
|
||||
}
|
||||
/>
|
||||
</Switch>
|
||||
{isCreateOpen && (
|
||||
<CreateModal
|
||||
onClose={this.toggleCreate}
|
||||
onStatusChange={onStatusChange}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@ -123,6 +132,12 @@ class AddressBookApp extends React.PureComponent<Props, State> {
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
private toggleCreate = (): void => {
|
||||
this.setState(({ isCreateOpen }) => ({
|
||||
isCreateOpen: !isCreateOpen
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
export default withMulti(
|
||||
|
||||
221
packages/app-address-book/src/modals/Create.tsx
Normal file
221
packages/app-address-book/src/modals/Create.tsx
Normal file
@ -0,0 +1,221 @@
|
||||
// Copyright 2017-2019 @polkadot/app-address-book 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 { I18nProps } from '@polkadot/ui-app/types';
|
||||
import { ActionStatus } from '@polkadot/ui-app/Status/types';
|
||||
import { ModalProps } from '../types';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { AddressRow, Button, Input, InputTags, Modal } from '@polkadot/ui-app';
|
||||
import { InputAddress } from '@polkadot/ui-app/InputAddress';
|
||||
import keyring from '@polkadot/ui-keyring';
|
||||
|
||||
import translate from '../translate';
|
||||
|
||||
type Props = ModalProps & I18nProps;
|
||||
|
||||
type State = {
|
||||
address: string,
|
||||
isAddressExisting: boolean,
|
||||
isAddressValid: boolean,
|
||||
isNameValid: boolean,
|
||||
isValid: boolean,
|
||||
name: string,
|
||||
tags: Array<string>
|
||||
};
|
||||
|
||||
class Create extends React.PureComponent<Props, State> {
|
||||
state: State;
|
||||
|
||||
constructor (props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = this.emptyState();
|
||||
}
|
||||
|
||||
render () {
|
||||
const { t } = this.props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
dimmer='inverted'
|
||||
open
|
||||
>
|
||||
<Modal.Header>{t('Add an address')}</Modal.Header>
|
||||
{this.renderContent()}
|
||||
{this.renderButtons()}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
private renderButtons () {
|
||||
const { t } = this.props;
|
||||
const { isValid } = this.state;
|
||||
|
||||
return (
|
||||
<Modal.Actions>
|
||||
<Button.Group>
|
||||
<Button
|
||||
isNegative
|
||||
onClick={this.onDiscard}
|
||||
label={t('Cancel')}
|
||||
/>
|
||||
<Button.Or />
|
||||
<Button
|
||||
isDisabled={!isValid}
|
||||
isPrimary
|
||||
onClick={this.onCommit}
|
||||
label={t('Save')}
|
||||
/>
|
||||
</Button.Group>
|
||||
</Modal.Actions>
|
||||
);
|
||||
}
|
||||
|
||||
private renderContent () {
|
||||
const { t } = this.props;
|
||||
const { address, isAddressValid, isNameValid, name, tags } = this.state;
|
||||
|
||||
return (
|
||||
<Modal.Content>
|
||||
<AddressRow
|
||||
defaultName={name}
|
||||
value={address}
|
||||
>
|
||||
<Input
|
||||
autoFocus
|
||||
className='full'
|
||||
help={t('Paste here the address of the contact you want to add to your address book.')}
|
||||
isError={!isAddressValid}
|
||||
label={t('address')}
|
||||
onChange={this.onChangeAddress}
|
||||
onEnter={this.onCommit}
|
||||
value={address}
|
||||
/>
|
||||
<Input
|
||||
className='full'
|
||||
help={t('Type the name of your contact. This name will be used across all the apps. It can be edited later on.')}
|
||||
isError={!isNameValid}
|
||||
label={t('name')}
|
||||
onChange={this.onChangeName}
|
||||
onEnter={this.onCommit}
|
||||
value={name}
|
||||
/>
|
||||
<InputTags
|
||||
help={t('Additional user-specified tags that can be used to identify the address. Tags can be used for categorization and filtering.')}
|
||||
label={t('user-defined tags')}
|
||||
onChange={this.onChangeTags}
|
||||
value={tags}
|
||||
/>
|
||||
</AddressRow>
|
||||
</Modal.Content>
|
||||
);
|
||||
}
|
||||
|
||||
private emptyState (): State {
|
||||
return {
|
||||
address: '',
|
||||
isAddressExisting: false,
|
||||
isAddressValid: false,
|
||||
isNameValid: true,
|
||||
isValid: false,
|
||||
name: 'new address',
|
||||
tags: []
|
||||
};
|
||||
}
|
||||
|
||||
private nextState (newState: State, allowEdit: boolean = false): void {
|
||||
this.setState(
|
||||
(prevState: State): State => {
|
||||
let { address = prevState.address, name = prevState.name, tags = prevState.tags } = newState;
|
||||
let isAddressValid = true;
|
||||
let isAddressExisting = false;
|
||||
let newAddress = address;
|
||||
|
||||
try {
|
||||
newAddress = keyring.encodeAddress(
|
||||
keyring.decodeAddress(address)
|
||||
);
|
||||
isAddressValid = keyring.isAvailable(newAddress);
|
||||
|
||||
if (!isAddressValid) {
|
||||
const old = keyring.getAddress(newAddress);
|
||||
|
||||
if (old.isValid) {
|
||||
if (!allowEdit) {
|
||||
name = old.getMeta().name || name;
|
||||
}
|
||||
|
||||
isAddressExisting = true;
|
||||
isAddressValid = true;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
isAddressValid = false;
|
||||
}
|
||||
|
||||
const isNameValid = !!name;
|
||||
|
||||
return {
|
||||
address: newAddress,
|
||||
isAddressExisting,
|
||||
isAddressValid,
|
||||
isNameValid,
|
||||
isValid: isAddressValid && isNameValid,
|
||||
name,
|
||||
tags
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private onChangeAddress = (address: string): void => {
|
||||
this.nextState({ address } as State);
|
||||
}
|
||||
|
||||
private onChangeName = (name: string): void => {
|
||||
this.nextState({ name } as State, true);
|
||||
}
|
||||
|
||||
private onChangeTags = (tags: Array<string>): void => {
|
||||
this.setState({ tags });
|
||||
}
|
||||
|
||||
private onCommit = (): void => {
|
||||
const { onClose, onStatusChange, t } = this.props;
|
||||
const { address, isAddressExisting, isValid, name, tags } = this.state;
|
||||
const status = { action: 'create' } as ActionStatus;
|
||||
|
||||
if (!isValid) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
keyring.saveAddress(address, { name, tags });
|
||||
|
||||
status.account = address;
|
||||
status.status = address ? 'success' : 'error';
|
||||
status.message = isAddressExisting
|
||||
? t('address edited')
|
||||
: t('address created');
|
||||
|
||||
InputAddress.setLastValue('address', address);
|
||||
} catch (error) {
|
||||
status.status = 'error';
|
||||
status.message = error.message;
|
||||
}
|
||||
|
||||
onStatusChange(status);
|
||||
onClose();
|
||||
}
|
||||
|
||||
private onDiscard = (): void => {
|
||||
const { onClose } = this.props;
|
||||
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
|
||||
export default translate(Create);
|
||||
@ -3,5 +3,11 @@
|
||||
// of the Apache-2.0 license. See the LICENSE file for details.
|
||||
|
||||
import { AppProps } from '@polkadot/ui-app/types';
|
||||
import { ActionStatus } from '@polkadot/ui-app/Status/types';
|
||||
|
||||
export type ComponentProps = AppProps;
|
||||
|
||||
export type ModalProps = {
|
||||
onClose: () => void,
|
||||
onStatusChange: (status: ActionStatus) => void
|
||||
};
|
||||
|
||||
@ -110,10 +110,6 @@
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
> .content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
> .header:not(.ui) {
|
||||
background: #f5f5f5;
|
||||
font-size: 1.25rem !important;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user