Align confirmation dialog with the UI (#3585)

* Align confirmation dialog with the UI

* Padding for all parts

* Align confirmation on derive

* Combine modals

* linting
This commit is contained in:
Jaco Greeff 2020-09-01 14:22:18 +02:00 committed by GitHub
parent eab82d853f
commit 0b6c295789
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 320 additions and 269 deletions

View File

@ -20,8 +20,8 @@ import { keyExtractSuri, mnemonicGenerate, mnemonicValidate, randomAsU8a } from
import { getEnvironment } from '@polkadot/react-api/util';
import { useTranslation } from '../translate';
import PasswordInput from '../Accounts/PasswordInput';
import CreateConfirmation from './CreateConfirmation';
import PasswordInput from './PasswordInput';
interface Props extends ModalProps {
className?: string;
@ -178,6 +178,7 @@ function Create ({ className = '', onClose, onStatusChange, seed: propsSeed, typ
const [{ isNameValid, name }, setName] = useState({ isNameValid: false, name: '' });
const [{ isPasswordValid, password }, setPassword] = useState({ isPasswordValid: false, password: '' });
const isValid = !!address && !deriveError && isNameValid && isPasswordValid && isSeedValid;
const seedOpt = useMemo(() => (
isDevelopment
? [{ text: t<string>('Development'), value: 'dev' }]
@ -232,13 +233,12 @@ function Create ({ className = '', onClose, onStatusChange, seed: propsSeed, typ
const options = { genesisHash: isDevelopment ? undefined : api.genesisHash.toString(), name: name.trim() };
const status = createAccount(`${seed}${derivePath}`, pairType, options, password, t<string>('created account'));
toggleConfirmation();
onStatusChange(status);
setIsBusy(false);
onClose();
}, 0);
},
[api, derivePath, isDevelopment, isValid, name, onClose, onStatusChange, pairType, password, seed, t, toggleConfirmation]
[api, derivePath, isDevelopment, isValid, name, onClose, onStatusChange, pairType, password, seed, t]
);
return (
@ -247,144 +247,166 @@ function Create ({ className = '', onClose, onStatusChange, seed: propsSeed, typ
header={t<string>('Add an account via seed')}
size='large'
>
{address && isConfirmationOpen && (
<CreateConfirmation
address={address}
isBusy={isBusy}
name={name}
onClose={toggleConfirmation}
onCommit={_onCommit}
/>
)}
<Modal.Content>
<Modal.Columns>
<Modal.Column>
<AddressRow
defaultName={name}
noDefaultNameOpacity
value={isSeedValid ? address : ''}
/>
</Modal.Column>
</Modal.Columns>
<Modal.Columns>
<Modal.Column>
<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}
{address && isConfirmationOpen
? (
<CreateConfirmation
address={address}
derivePath={derivePath}
isBusy={isBusy}
name={name}
pairType={pairType}
seed={seed}
/>
)
: (
<Modal.Content>
<Modal.Columns>
<Modal.Column>
<AddressRow
defaultName={name}
noDefaultNameOpacity
value={isSeedValid ? address : ''}
/>
</Modal.Column>
</Modal.Columns>
<Modal.Columns>
<Modal.Column>
<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.Column>
<Modal.Column>
<p>{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.')}</p>
</Modal.Column>
</Modal.Columns>
<Modal.Columns>
<Modal.Column>
<Input
help={t<string>('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.')}
isAction
isError={!isSeedValid}
isReadOnly={seedType === 'dev'}
label={
seedType === 'bip'
? t<string>('mnemonic seed')
: seedType === 'dev'
? t<string>('development seed')
: t<string>('seed (hex or string)')
}
onChange={_onChangeSeed}
onEnter={_onCommit}
value={seed}
>
<Dropdown
defaultValue={seedType}
isButton
onChange={_selectSeedType}
options={seedOpt}
/>
<CopyButton
className='copyMoved'
value={seed}
/>
</Input>
</Modal.Column>
<Modal.Column>
<p>{t<string>('The secret seed value for this account. Ensure that you keep this in a safe place, with access to the seed you can re-create the account.')}</p>
</Modal.Column>
</Modal.Columns>
<PasswordInput
onChange={_onPasswordChange}
onEnter={_onCommit}
placeholder={t<string>('new account')}
value={name}
/>
</Modal.Column>
<Modal.Column>
<p>{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.')}</p>
</Modal.Column>
</Modal.Columns>
<Modal.Columns>
<Modal.Column>
<Input
help={t<string>('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.')}
isAction
isError={!isSeedValid}
isReadOnly={seedType === 'dev'}
label={
seedType === 'bip'
? t<string>('mnemonic seed')
: seedType === 'dev'
? t<string>('development seed')
: t<string>('seed (hex or string)')
}
onChange={_onChangeSeed}
onEnter={_onCommit}
value={seed}
{!isElectron && (
<article className='warning'>
<p>{t<string>('Consider storing your account in a signer such as a browser extension, hardware device, QR-capable phone wallet (non-connected) or desktop application for optimal account security.')}&nbsp;{t<string>('Future versions of the web-only interface will drop support for non-external accounts, much like the IPFS version.')}</p>
</article>
)}
<Expander
className='accounts--Creator-advanced'
isPadded
summary={t<string>('Advanced creation options')}
>
<Dropdown
defaultValue={seedType}
isButton
onChange={_selectSeedType}
options={seedOpt}
/>
<CopyButton
className='copyMoved'
value={seed}
/>
</Input>
</Modal.Column>
<Modal.Column>
<p>{t<string>('The secret seed value for this account. Ensure that you keep this in a safe place, with access to the seed you can re-create the account.')}</p>
</Modal.Column>
</Modal.Columns>
<PasswordInput
onChange={_onPasswordChange}
onEnter={_onCommit}
password={password}
/>
{!isElectron && (
<article className='warning'>
<p>{t<string>('Consider storing your account in a signer such as a browser extension, hardware device, QR-capable phone wallet (non-connected) or desktop application for optimal account security.')}&nbsp;{t<string>('Future versions of the web-only interface will drop support for non-external accounts, much like the IPFS version.')}</p>
</article>
)}
<Expander
className='accounts--Creator-advanced'
isPadded
summary={t<string>('Advanced creation options')}
>
<Modal.Columns>
<Modal.Column>
<Dropdown
defaultValue={pairType}
help={t<string>('Determines what cryptography will be used to create this account. Note that to validate on Polkadot, the session account must use "ed25519".')}
label={t<string>('keypair crypto type')}
onChange={_onChangePairType}
options={uiSettings.availableCryptos}
tabIndex={-1}
/>
</Modal.Column>
<Modal.Column>
<p>{t<string>('If you are moving accounts between applications, ensure that you use the correct type.')}</p>
</Modal.Column>
</Modal.Columns>
<Modal.Columns>
<Modal.Column>
<Input
help={t<string>('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`. An optional "///<password>" can be used with a mnemonic seed, and may only be specified once.')}
isError={!!deriveError}
label={t<string>('secret derivation path')}
onChange={_onChangeDerive}
onEnter={_onCommit}
placeholder={
seedType === 'raw'
? pairType === 'sr25519'
? t<string>('//hard/soft')
: t<string>('//hard')
: pairType === 'sr25519'
? t<string>('//hard/soft///password')
: t<string>('//hard///password')
}
tabIndex={-1}
value={derivePath}
/>
{deriveError && (
<article className='error'>{deriveError}</article>
)}
</Modal.Column>
<Modal.Column>
<p>{t<string>('The derivation path allows you to create different accounts from the same base mnemonic.')}</p>
</Modal.Column>
</Modal.Columns>
</Expander>
</Modal.Content>
<Modal.Columns>
<Modal.Column>
<Dropdown
defaultValue={pairType}
help={t<string>('Determines what cryptography will be used to create this account. Note that to validate on Polkadot, the session account must use "ed25519".')}
label={t<string>('keypair crypto type')}
onChange={_onChangePairType}
options={uiSettings.availableCryptos}
tabIndex={-1}
/>
</Modal.Column>
<Modal.Column>
<p>{t<string>('If you are moving accounts between applications, ensure that you use the correct type.')}</p>
</Modal.Column>
</Modal.Columns>
<Modal.Columns>
<Modal.Column>
<Input
help={t<string>('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`. An optional "///<password>" can be used with a mnemonic seed, and may only be specified once.')}
isError={!!deriveError}
label={t<string>('secret derivation path')}
onChange={_onChangeDerive}
onEnter={_onCommit}
placeholder={
seedType === 'raw'
? pairType === 'sr25519'
? t<string>('//hard/soft')
: t<string>('//hard')
: pairType === 'sr25519'
? t<string>('//hard/soft///password')
: t<string>('//hard///password')
}
tabIndex={-1}
value={derivePath}
/>
{deriveError && (
<article className='error'>{deriveError}</article>
)}
</Modal.Column>
<Modal.Column>
<p>{t<string>('The derivation path allows you to create different accounts from the same base mnemonic.')}</p>
</Modal.Column>
</Modal.Columns>
</Expander>
</Modal.Content>
)
}
<Modal.Actions onCancel={onClose}>
<Button
icon='plus'
isBusy={isBusy}
isDisabled={!isValid}
label={t<string>('Save')}
onClick={toggleConfirmation}
/>
{isConfirmationOpen
? (
<>
<Button
icon='step-backward'
label={t<string>('Prev')}
onClick={toggleConfirmation}
/>
<Button
icon='plus'
isBusy={isBusy}
label={t<string>('Save')}
onClick={_onCommit}
/>
</>
)
: (
<Button
icon='step-forward'
isDisabled={!isValid}
label={t<string>('Next')}
onClick={toggleConfirmation}
/>
)
}
</Modal.Actions>
</Modal>
);

View File

@ -2,44 +2,62 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { KeypairType } from '@polkadot/util-crypto/types';
import React from 'react';
import { AddressRow, Button, Modal } from '@polkadot/react-components';
import { AddressRow, Modal, Static } from '@polkadot/react-components';
import { isHex } from '@polkadot/util';
import { useTranslation } from '../translate';
interface Props {
address: string;
derivePath: string;
isBusy: boolean;
name: string;
onClose: () => void;
onCommit: () => void;
pairType: KeypairType;
seed?: string;
}
function CreateConfirmation ({ address, isBusy, name, onClose, onCommit }: Props): React.ReactElement<Props> | null {
function CreateConfirmation ({ address, derivePath, name, pairType, seed }: Props): React.ReactElement<Props> | null {
const { t } = useTranslation();
const splitSeed = seed && seed.split(' ');
const shortSeed = isHex(seed)
? `${seed.substr(10)} … ${seed.substr(-8)}`
: splitSeed && splitSeed.map((value, index) => (index % 3) ? '…' : value).join(' ');
return (
<Modal header={t<string>('Important notice')}>
<Modal.Content>
<AddressRow
defaultName={name}
isInline
noDefaultNameOpacity
value={address}
>
<Modal.Content>
<Modal.Columns>
<Modal.Column>
<AddressRow
defaultName={name}
isInline
noDefaultNameOpacity
value={address}
/>
{shortSeed && (
<Static
label={t<string>('partial seed')}
value={shortSeed}
/>
)}
<Static
label={t<string>('keypair type')}
value={pairType}
/>
<Static
label={t<string>('derivation path')}
value={derivePath || t<string>('<none provided>')}
/>
</Modal.Column>
<Modal.Column>
<p>{t<string>('We will provide you with a generated backup file after your account is created. As long as you have access to your account you can always download this file later by clicking on "Backup" button from the Accounts section.')}</p>
<p>{t<string>('Please make sure to save this file in a secure location as it is required, together with your password, to restore your account.')}</p>
</AddressRow>
</Modal.Content>
<Modal.Actions onCancel={onClose}>
<Button
icon='plus'
isBusy={isBusy}
label={t<string>('Create and backup account')}
onClick={onCommit}
/>
</Modal.Actions>
</Modal>
</Modal.Column>
</Modal.Columns>
</Modal.Content>
);
}

View File

@ -160,13 +160,12 @@ function Derive ({ className = '', from, onClose }: Props): React.ReactElement {
setTimeout((): void => {
const status = createAccount(source, suri, name, password, t<string>('created account'), isDevelopment ? undefined : api.genesisHash.toString());
toggleConfirmation();
queueAction(status);
setIsBusy(false);
onClose();
}, 0);
},
[api, isDevelopment, isValid, name, onClose, password, queueAction, source, suri, t, toggleConfirmation]
[api, isDevelopment, isValid, name, onClose, password, queueAction, source, suri, t]
);
const sourceStatic = (
@ -183,74 +182,78 @@ function Derive ({ className = '', from, onClose }: Props): React.ReactElement {
className={className}
header={t<string>('Derive account from pair')}
>
{address && isConfirmationOpen && (
<CreateConfirmation
address={address}
isBusy={isBusy}
name={name}
onClose={toggleConfirmation}
onCommit={_onCommit}
/>
)}
<Modal.Content>
{isLocked && (
<>
{sourceStatic}
<Password
autoFocus
help={t<string>('The password to unlock the selected account.')}
isError={!!lockedError}
label={t<string>('password')}
onChange={_onChangeRootPass}
value={rootPass}
/>
</>
)}
{!isLocked && (
<AddressRow
defaultName={name}
noDefaultNameOpacity
value={deriveError ? '' : address}
>
{sourceStatic}
<Input
autoFocus
help={t<string>('You can set a custom derivation path for this account using the following syntax "/<soft-key>//<hard-key>///<password>". The "/<soft-key>" and "//<hard-key>" may be repeated and mixed`. The "///password" is optional and should only occur once.')}
label={t<string>('derivation path')}
onChange={setSuri}
placeholder={t<string>('//hard/soft')}
/>
<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}
/>
</AddressRow>
)}
</Modal.Content>
{address && isConfirmationOpen
? (
<CreateConfirmation
address={address}
derivePath={suri}
isBusy={isBusy}
name={name}
pairType={source.type}
/>
)
: (
<Modal.Content>
{isLocked && (
<>
{sourceStatic}
<Password
autoFocus
help={t<string>('The password to unlock the selected account.')}
isError={!!lockedError}
label={t<string>('password')}
onChange={_onChangeRootPass}
value={rootPass}
/>
</>
)}
{!isLocked && (
<AddressRow
defaultName={name}
noDefaultNameOpacity
value={deriveError ? '' : address}
>
{sourceStatic}
<Input
autoFocus
help={t<string>('You can set a custom derivation path for this account using the following syntax "/<soft-key>//<hard-key>///<password>". The "/<soft-key>" and "//<hard-key>" may be repeated and mixed`. The "///password" is optional and should only occur once.')}
label={t<string>('derivation path')}
onChange={setSuri}
placeholder={t<string>('//hard/soft')}
/>
<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}
/>
</AddressRow>
)}
</Modal.Content>
)
}
<Modal.Actions onCancel={onClose}>
{isLocked
? (
@ -262,14 +265,30 @@ function Derive ({ className = '', from, onClose }: Props): React.ReactElement {
onClick={_onUnlock}
/>
)
: (
<Button
icon='plus'
isBusy={isBusy}
isDisabled={!isValid}
label={t<string>('Save')}
onClick={toggleConfirmation}
/>
: (isConfirmationOpen
? (
<>
<Button
icon='step-backward'
label={t<string>('Prev')}
onClick={toggleConfirmation}
/>
<Button
icon='plus'
isBusy={isBusy}
label={t<string>('Save')}
onClick={_onCommit}
/>
</>
)
: (
<Button
icon='step-forward'
isDisabled={!isValid}
label={t<string>('Next')}
onClick={toggleConfirmation}
/>
)
)
}
</Modal.Actions>

View File

@ -2,47 +2,41 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import React, { useCallback, useState } from 'react';
import React, { useCallback, useEffect, useState } from 'react';
import { Modal, Password, PasswordStrength } from '@polkadot/react-components';
import keyring from '@polkadot/ui-keyring';
import { useTranslation } from '../translate';
type Props = {
password: string;
onChange: (password: string, isPasswordValid: boolean) => void;
onEnter: () => void;
}
export default function PasswordInput ({ onChange, onEnter, password }: Props): React.ReactElement {
export default function PasswordInput ({ onChange, onEnter }: Props): React.ReactElement {
const { t } = useTranslation();
const [isPassValid, setPassValid] = useState<boolean>(false);
const [{ isPass1Valid, password1 }, setPassword1] = useState({ isPass1Valid: false, password1: '' });
const [{ isPass2Valid, password2 }, setPassword2] = useState({ isPass2Valid: false, password2: '' });
const _onPasswordChange = useCallback(
(password: string) => {
const isPassValid = keyring.isPassValid(password);
useEffect(
() => onChange(password1, isPass1Valid && isPass2Valid),
[password1, onChange, isPass1Valid, isPass2Valid]
);
setPassValid(isPassValid);
const isValid = isPassValid && isPass2Valid;
onChange(password, isValid);
},
[onChange, isPass2Valid]
const _onPassword1Change = useCallback(
(password1: string) => setPassword1({
isPass1Valid: keyring.isPassValid(password1),
password1
}),
[]
);
const onPassword2Change = useCallback(
(password2: string) => {
const isPass2Valid = keyring.isPassValid(password2) && (password2 === password);
setPassword2({ isPass2Valid, password2 });
const isValid = isPassValid && isPass2Valid;
onChange(password, isValid);
},
[password, onChange, isPassValid]
(password2: string) => setPassword2({
isPass2Valid: keyring.isPassValid(password2) && (password2 === password1),
password2
}),
[password1]
);
return (
@ -51,11 +45,11 @@ export default function PasswordInput ({ onChange, onEnter, password }: Props):
<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}
isError={!isPass1Valid}
label={t<string>('password')}
onChange={_onPasswordChange}
onChange={_onPassword1Change}
onEnter={onEnter}
value={password}
value={password1}
/>
<Password
className='full'
@ -66,7 +60,7 @@ export default function PasswordInput ({ onChange, onEnter, password }: Props):
onEnter={onEnter}
value={password2}
/>
<PasswordStrength value={password} />
<PasswordStrength value={password1} />
</Modal.Column>
<Modal.Column>
<p>{t<string>('The password and password confirmation for this account. This is required to authenticate any transactions made and to encrypt the keypair.')}</p>

View File

@ -11,8 +11,8 @@ import { AddressRow, Button, Input, InputAddress, Modal, QrScanAddress } from '@
import { useApi, useIpfs } from '@polkadot/react-hooks';
import keyring from '@polkadot/ui-keyring';
import PasswordInput from '../Accounts/PasswordInput';
import { useTranslation } from '../translate';
import PasswordInput from './PasswordInput';
interface Scanned {
content: string;
@ -41,7 +41,6 @@ function QrModal ({ className = '', onClose, onStatusChange }: Props): React.Rea
const [{ address, isAddress, scanned }, setAddress] = useState<Address>({ address: '', isAddress: false, scanned: null });
const [{ isPasswordValid, password }, setPassword] = useState({ isPasswordValid: false, password: '' });
// FIXME This gets passed to the useCallback, so we have excessive creations
const isValid = !!address && isNameValid && (isAddress || isPasswordValid);
const _onNameChange = useCallback(
@ -139,7 +138,6 @@ function QrModal ({ className = '', onClose, onStatusChange }: Props): React.Rea
<PasswordInput
onChange={_onPasswordChange}
onEnter={_onSave}
password={password}
/>
)}
</>