Remove code duplication in Create and Derive modals (#5895)

* Factor out code duplication in createAccount

* Factor out account names

* Factor out account pass

* Factor out common UI elements

* Move CreateAccountInputs to a separate file

* Rename "getResult" to "commitAccount"

* Rename "AccountPass" to "AccountPassword"

* Use shorter name for component Props

* Rename "isPassValid" to "isPasswordValid"

* Update packages/page-accounts/src/modals/CreateAccountInputs.tsx

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

* Update packages/page-accounts/src/util.tsx

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

* Rename _onCommit prop to onCommit

* Apply new lint rules

Co-authored-by: Jaco <jacogr@gmail.com>
Co-authored-by: MiZiet <michalzietalak@o2.pl>
This commit is contained in:
Vitaliy 2021-09-01 15:25:40 +02:00 committed by GitHub
parent 9060edff3f
commit f248714613
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 129 additions and 126 deletions

View File

@ -2,16 +2,13 @@
// SPDX-License-Identifier: Apache-2.0
import type { ActionStatus } from '@polkadot/react-components/Status/types';
import type { CreateResult } from '@polkadot/ui-keyring/types';
import type { AddressState, CreateOptions, CreateProps, DeriveValidationOutput, PairType, SeedType } from '../types';
import FileSaver from 'file-saver';
import React, { useCallback, useRef, useState } from 'react';
import styled from 'styled-components';
import { DEV_PHRASE } from '@polkadot/keyring/defaults';
import { getEnvironment } from '@polkadot/react-api/util';
import { AddressRow, Button, Checkbox, CopyButton, Dropdown, Expander, Input, InputAddress, MarkError, MarkWarning, Modal, TextArea } from '@polkadot/react-components';
import { AddressRow, Button, Checkbox, CopyButton, Dropdown, Expander, Input, MarkError, MarkWarning, Modal, TextArea } from '@polkadot/react-components';
import { useApi, useLedger, useStepper } from '@polkadot/react-hooks';
import { keyring } from '@polkadot/ui-keyring';
import { settings } from '@polkadot/ui-settings';
@ -19,11 +16,12 @@ import { isHex, u8aToHex } from '@polkadot/util';
import { hdLedger, hdValidatePath, keyExtractSuri, mnemonicGenerate, mnemonicValidate, randomAsU8a } from '@polkadot/util-crypto';
import { useTranslation } from '../translate';
import { tryCreateAccount } from '../util';
import CreateAccountInputs from './CreateAccountInputs';
import CreateConfirmation from './CreateConfirmation';
import CreateEthDerivationPath, { ETH_DEFAULT_PATH } from './CreateEthDerivationPath';
import CreateSuriLedger from './CreateSuriLedger';
import ExternalWarning from './ExternalWarning';
import PasswordInput from './PasswordInput';
const DEFAULT_PAIR_TYPE = 'sr25519';
const STEPS_COUNT = 3;
@ -134,35 +132,11 @@ function updateAddress (seed: string, derivePath: string, seedType: SeedType, pa
};
}
export function downloadAccount ({ json, pair }: CreateResult): void {
const blob = new Blob([JSON.stringify(json)], { type: 'application/json; charset=utf-8' });
FileSaver.saveAs(blob, `${pair.address}.json`);
}
function createAccount (seed: string, derivePath: string, pairType: PairType, { genesisHash, name, tags = [] }: CreateOptions, password: string, success: string): ActionStatus {
// we will fill in all the details below
const status = { action: 'create' } as ActionStatus;
const commitAccount = () =>
keyring.addUri(getSuri(seed, derivePath, pairType), password, { genesisHash, isHardware: false, name, tags }, pairType === 'ed25519-ledger' ? 'ed25519' : pairType);
try {
const result = keyring.addUri(getSuri(seed, derivePath, pairType), password, { genesisHash, isHardware: false, name, tags }, pairType === 'ed25519-ledger' ? 'ed25519' : pairType);
const { address } = result.pair;
status.account = address;
status.status = 'success';
status.message = success;
InputAddress.setLastValue('account', address);
if (getEnvironment() === 'web') {
downloadAccount(result);
}
} catch (error) {
status.status = 'error';
status.message = (error as Error).message;
}
return status;
return tryCreateAccount(commitAccount, success);
}
function Create ({ className = '', onClose, onStatusChange, seed: propsSeed, type: propsType }: CreateProps): React.ReactElement<CreateProps> {
@ -231,16 +205,6 @@ function Create ({ className = '', onClose, onStatusChange, seed: propsSeed, typ
[derivePath, pairType, seedType]
);
const _onChangeName = useCallback(
(name: string) => setName({ isNameValid: !!name.trim(), name }),
[]
);
const _onPasswordChange = useCallback(
(password: string, isPasswordValid: boolean) => setPassword({ isPasswordValid, password }),
[]
);
const _toggleMnemonicSaved = useCallback(
() => setIsMnemonicSaved(!isMnemonicSaved),
[isMnemonicSaved]
@ -397,22 +361,12 @@ function Create ({ className = '', onClose, onStatusChange, seed: propsSeed, typ
</Modal.Columns>
</>}
{step === 2 && <>
<Modal.Columns hint={t<string>('The name for this account and how it will appear under your addresses. With an on-chain identity, it can be made available to others.')}>
<Input
autoFocus
help={t<string>('Name given to this account. You can edit it. To use the account to validate or nominate, it is a good practice to append the function of the account in the name, e.g "name_you_want - stash".')}
isError={!isNameValid}
label={t<string>('name')}
onChange={_onChangeName}
onEnter={_onCommit}
placeholder={t<string>('new account')}
value={name}
/>
</Modal.Columns>
<PasswordInput
onChange={_onPasswordChange}
onEnter={_onCommit}
/>
<CreateAccountInputs
name={{ isNameValid, name }}
onCommit={_onCommit}
setName={setName}
setPassword={setPassword}
/>;
<Modal.Columns>
<ExternalWarning />
</Modal.Columns>

View File

@ -0,0 +1,63 @@
// Copyright 2017-2021 @polkadot/app-accounts authors & contributors
// SPDX-License-Identifier: Apache-2.0
import React, { useCallback } from 'react';
import { Input, Modal } from '@polkadot/react-components';
import { useTranslation } from '@polkadot/react-components/translate';
import PasswordInput from './PasswordInput';
interface AccountName {
name: string;
isNameValid: boolean;
}
interface AccountPassword {
password: string;
isPasswordValid: boolean;
}
interface Props {
name: AccountName;
onCommit: () => void;
setName: (value: AccountName) => void;
setPassword: (value: AccountPassword) => void;
}
const CreateAccountInputs = ({ name: { isNameValid, name }, onCommit, setName, setPassword }: Props) => {
const { t } = useTranslation();
const _onChangeName = useCallback(
(name: string) => setName({ isNameValid: !!name.trim(), name }),
[setName]
);
const _onChangePass = useCallback(
(password: string, isValid: boolean) => setPassword({ isPasswordValid: isValid, password }),
[setPassword]
);
return (
<>
<Modal.Columns hint={t<string>('The name for this account and how it will appear under your addresses. With an on-chain identity, it can be made available to others.')}>
<Input
className='full'
help={t<string>('Name given to this account. You can edit it. To use the account to validate or nominate, it is a good practice to append the function of the account in the name, e.g "name_you_want - stash".')}
isError={!isNameValid}
label={t<string>('name')}
onChange={_onChangeName}
onEnter={onCommit}
placeholder={t<string>('new account')}
value={name}
/>
</Modal.Columns>
<PasswordInput
onChange={_onChangePass}
onEnter={onCommit}
/>
</>
);
};
export default React.memo(CreateAccountInputs);

View File

@ -13,7 +13,8 @@ import { keyring } from '@polkadot/ui-keyring';
import { keyExtractPath } from '@polkadot/util-crypto';
import { useTranslation } from '../translate';
import { downloadAccount } from './Create';
import { tryCreateAccount } from '../util';
import CreateAccountInputs from './CreateAccountInputs';
import CreateConfirmation from './CreateConfirmation';
interface Props {
@ -54,29 +55,15 @@ function deriveValidate (suri: string, pairType: KeypairType): string | null {
}
function createAccount (source: KeyringPair, suri: string, name: string, password: string, success: string, genesisHash?: string): ActionStatus {
// we will fill in all the details below
const status = { action: 'create' } as ActionStatus;
try {
const commitAccount = () => {
const derived = source.derive(suri);
derived.setMeta({ ...derived.meta, genesisHash, name, parentAddress: source.address, tags: [] });
const result = keyring.addPair(derived, password || '');
const { address } = result.pair;
return keyring.addPair(derived, password || '');
};
status.account = address;
status.status = 'success';
status.message = success;
downloadAccount(result);
InputAddress.setLastValue('account', address);
} catch (error) {
status.status = 'error';
status.message = (error as Error).message;
}
return status;
return tryCreateAccount(commitAccount, success);
}
function Derive ({ className = '', from, onClose }: Props): React.ReactElement {
@ -85,16 +72,15 @@ function Derive ({ className = '', from, onClose }: Props): React.ReactElement {
const { queueAction } = useContext(StatusContext);
const [source] = useState(() => keyring.getPair(from));
const [isBusy, setIsBusy] = useState(false);
const [{ isNameValid, name }, setName] = useState({ isNameValid: false, name: '' });
const [{ isPasswordValid, password }, setPassword] = useState({ isPasswordValid: false, password: '' });
const [{ address, deriveError }, setDerive] = useState<DeriveAddress>({ address: null, deriveError: null });
const [isConfirmationOpen, toggleConfirmation] = useToggle();
const [{ isLocked, lockedError }, setIsLocked] = useState<LockState>({ isLocked: source.isLocked, lockedError: null });
const [{ isNameValid, name }, setName] = useState({ isNameValid: false, name: '' });
const [{ isPassValid, password }, setPassword] = useState({ isPassValid: false, password: '' });
const [{ isPass2Valid, password2 }, setPassword2] = useState({ isPass2Valid: false, password2: '' });
const [{ isRootValid, rootPass }, setRootPass] = useState({ isRootValid: false, rootPass: '' });
const [suri, setSuri] = useState('');
const debouncedSuri = useDebounce(suri);
const isValid = !!address && !deriveError && isNameValid && isPassValid && isPass2Valid;
const isValid = !!address && !deriveError && isNameValid && isPasswordValid;
useEffect((): void => {
setIsLocked({ isLocked: source.isLocked, lockedError: null });
@ -115,21 +101,6 @@ function Derive ({ className = '', from, onClose }: Props): React.ReactElement {
});
}, [debouncedSuri, isLocked, source]);
const _onChangeName = useCallback(
(name: string) => setName({ isNameValid: !!name.trim(), name }),
[]
);
const _onChangePass = useCallback(
(password: string) => setPassword({ isPassValid: keyring.isPassValid(password), password }),
[]
);
const _onChangePass2 = useCallback(
(password2: string) => setPassword2({ isPass2Valid: keyring.isPassValid(password2) && (password2 === password), password2 }),
[password]
);
const _onChangeRootPass = useCallback(
(rootPass: string): void => {
setRootPass({ isRootValid: !!rootPass, rootPass });
@ -231,34 +202,12 @@ function Derive ({ className = '', from, onClose }: Props): React.ReactElement {
{deriveError && (
<MarkError content={deriveError} />
)}
<Input
className='full'
help={t<string>('Name given to this account. You can edit it. To use the account to validate or nominate, it is a good practice to append the function of the account in the name, e.g "name_you_want - stash".')}
isError={!isNameValid}
label={t<string>('name')}
onChange={_onChangeName}
onEnter={_onCommit}
placeholder={t<string>('new account')}
value={name}
/>
<Password
className='full'
help={t<string>('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<string>('password')}
onChange={_onChangePass}
onEnter={_onCommit}
value={password}
/>
<Password
className='full'
help={t<string>('Verify the password entered above.')}
isError={!isPass2Valid}
label={t<string>('password (repeat)')}
onChange={_onChangePass2}
onEnter={_onCommit}
value={password2}
/>
<CreateAccountInputs
name={{ isNameValid, name }}
onCommit={_onCommit}
setName={setName}
setPassword={setPassword}
/>;
</AddressRow>
)}
</Modal.Content>

View File

@ -1,12 +1,15 @@
// Copyright 2017-2021 @polkadot/app-accounts authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { KeyringAddress } from '@polkadot/ui-keyring/types';
import type { ActionStatus } from '@polkadot/react-components/Status/types';
import type { CreateResult, KeyringAddress } from '@polkadot/ui-keyring/types';
import type { SortedAccount } from './types';
import FileSaver from 'file-saver';
import React from 'react';
import { Menu } from '@polkadot/react-components';
import { getEnvironment } from '@polkadot/react-api/util';
import { InputAddress, Menu } from '@polkadot/react-components';
import { keyring } from '@polkadot/ui-keyring';
export function createMenuGroup (key: string, items: (React.ReactNode | false | undefined | null)[], header?: string): React.ReactNode | null {
@ -21,6 +24,40 @@ export function createMenuGroup (key: string, items: (React.ReactNode | false |
: null;
}
export function downloadAccount ({ json, pair }: CreateResult): void {
FileSaver.saveAs(
new Blob([JSON.stringify(json)], { type: 'application/json; charset=utf-8' }),
`${pair.address}.json`
);
}
export function tryCreateAccount (commitAccount: () => CreateResult, success: string): ActionStatus {
const status: ActionStatus = {
action: 'create',
message: success,
status: 'success'
};
try {
const result = commitAccount();
const address = result.pair.address;
status.account = address;
if (getEnvironment() === 'web') {
downloadAccount(result);
}
downloadAccount(result);
InputAddress.setLastValue('account', address);
} catch (error) {
status.status = 'error';
status.message = (error as Error).message;
}
return status;
}
function expandList (mapped: SortedAccount[], entry: SortedAccount): SortedAccount[] {
mapped.push(entry);