Updated flow for creating accounts (#3880)
* Add 3 step account creation * Add textarea with dropdown * Add OWASP and styles to PasswordStrength * Align the checkbox with other ui elements * Notification on mnemonic copy * Make confirmation label clickable * Do not shorten address in creation modal * Make address name not editable in header * Always show password strength meter * Always show password strength meter * Update tests to new account flow * Remove repeated dependency * Remove console.log from test * Update yarn.lock * Restore imports order. * Format div component according to style * Remove cursor styling for old browsers * Empty line * Organize imports. Configure StrengthTester once. Use color from theme. * wip on text area * TextArea styling * Checbox styling * Update repeated password validity on first password change * Simplify CopyButton notification code. * Change TextArea styling to more consistent * Make password strength indicator wider * Move external warning down and show on both steps * Inline TextArea subcomponents * Move mnemonic saved confirmation label closer to buttons * Add steps counter to header * Remove space Co-authored-by: Michał Ziętalak <michal.z@ethworks.io> Co-authored-by: Jaco Greeff <jacogr@gmail.com>
This commit is contained in:
parent
9c3bfe76ad
commit
7b387762bd
@ -16,11 +16,11 @@ const internalModules = findPackages()
|
||||
const defaultConfig = {
|
||||
moduleNameMapper: {
|
||||
...internalModules,
|
||||
'@polkadot/apps/(.*)$': '<rootDir>/packages/apps/src/$1',
|
||||
'\\.(css|less)$': 'empty/object',
|
||||
'\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': 'empty/object',
|
||||
'\\.(md)$': '<rootDir>/__mocks__/fileMock.js'
|
||||
},
|
||||
testTimeout: 25000,
|
||||
transformIgnorePatterns: [
|
||||
'<rootDir>/node_modules'
|
||||
]
|
||||
@ -38,5 +38,6 @@ module.exports = Object.assign({}, config, {
|
||||
...defaultConfig,
|
||||
displayName: 'fast-tests'
|
||||
}
|
||||
]
|
||||
],
|
||||
testTimeout: 25000
|
||||
});
|
||||
|
||||
@ -36,7 +36,7 @@
|
||||
"Add a tip to this extrinsic, paying the block author for greater priority": "أضف جائزة إلى هذا البرنامج الخارجي، لتدفع لكاتب الكتلة في مقابل أولوية أكبر",
|
||||
"Add account": "أضف حسابًا",
|
||||
"Add account via Qr": "QR أضف حسابًا عبر كود",
|
||||
"Add an account via seed": "seed أضف حسابًا باستخدام الجملة البذرة",
|
||||
"Add an account via seed {{step}}/{{STEPS_COUNT}}": "{{step}}/{{STEPS_COUNT}} seed أضف حسابًا باستخدام الجملة البذرة",
|
||||
"Add an address": "أضف عنوانا",
|
||||
"Add an existing code hash": "إضافة تجزئة رمز موجودة",
|
||||
"Add an existing contract": "إضافة عقد موجود",
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
"Add account": "Add account",
|
||||
"Add account via Ledger": "Add account via Ledger",
|
||||
"Add account via Qr": "Add account via Qr",
|
||||
"Add an account via seed": "Add an account via seed",
|
||||
"Add an account via seed {{step}}/{{STEPS_COUNT}}": "Add an account via seed {{step}}/{{STEPS_COUNT}}",
|
||||
"Add identity judgment": "Add identity judgment",
|
||||
"Add multisig": "Add multisig",
|
||||
"Add proxied account": "Add proxied account",
|
||||
@ -63,6 +63,7 @@
|
||||
"Forget this account": "Forget this account",
|
||||
"Full Legal Name": "Full Legal Name",
|
||||
"Future versions of the web-only interface will drop support for non-external accounts, much like the IPFS version.": "Future versions of the web-only interface will drop support for non-external accounts, much like the IPFS version.",
|
||||
"I have saved my mnemonic seed safely": "I have saved my mnemonic seed safely",
|
||||
"If the delegated account is currently voting in a referendum, the delegating vote and conviction will be added.": "If the delegated account is currently voting in a referendum, the delegating vote and conviction will be added.",
|
||||
"If the recipient account is new, the balance needs to be more than the existential deposit. Likewise if the sending account balance drops below the same value, the account will be removed from the state.": "If the recipient account is new, the balance needs to be more than the existential deposit. Likewise if the sending account balance drops below the same value, the account will be removed from the state.",
|
||||
"If you add several proxy accounts for the same proxy type (e.g 2 accounts set as proxy for Governance), then any of those 2 accounts will be able to perfom governance actions on behalf of the proxied account": "If you add several proxy accounts for the same proxy type (e.g 2 accounts set as proxy for Governance), then any of those 2 accounts will be able to perfom governance actions on behalf of the proxied account",
|
||||
|
||||
@ -41,6 +41,7 @@
|
||||
"lifetime": "lifetime",
|
||||
"lock expired": "lock expired",
|
||||
"locked": "locked",
|
||||
"mnemonic copied": "mnemonic copied",
|
||||
"no tags": "no tags",
|
||||
"ongoing referendum": "ongoing referendum",
|
||||
"only this network": "only this network",
|
||||
@ -50,6 +51,7 @@
|
||||
"reserved": "reserved",
|
||||
"session keys": "session keys",
|
||||
"session next": "session next",
|
||||
"strong": "strong",
|
||||
"tip": "tip",
|
||||
"total": "total",
|
||||
"transactions": "transactions",
|
||||
@ -68,6 +70,7 @@
|
||||
"vote value": "vote value",
|
||||
"vote with account": "vote with account",
|
||||
"voting balance": "voting balance",
|
||||
"weak": "weak",
|
||||
"{{blocks}} blocks": "{{blocks}} blocks",
|
||||
"{{eras}} eras remaining": "{{eras}} eras remaining",
|
||||
"{{name}} ({{size}} bytes)": "{{name}} ({{size}} bytes)",
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
"Add a tip to this extrinsic, paying the block author for greater priority": "Añade una propina a este extrinsic, pagando al autor del bloque por una mayor prioridad",
|
||||
"Add account": "Añadir cuenta",
|
||||
"Add account via Qr": "Añadir cuenta desde el Qr",
|
||||
"Add an account via seed": "Añadir cuenta desde la semilla",
|
||||
"Add an account via seed {{step}}/{{STEPS_COUNT}}": "Añadir cuenta desde la semilla {{step}}/{{STEPS_COUNT}}",
|
||||
"Add an address": "Añadir una dirección",
|
||||
"Add an existing code hash": "Añadir un hash de código existente",
|
||||
"Add an existing contract": "Añadir un contrato existente",
|
||||
|
||||
@ -43,7 +43,7 @@
|
||||
"Add account": "Ajouter un compte",
|
||||
"Add account via Ledger": "Ajouter un compte via Ledger",
|
||||
"Add account via Qr": "Ajouter via Code QR",
|
||||
"Add an account via seed": "Ajouter un compte via phrase secrète",
|
||||
"Add an account via seed {{step}}/{{STEPS_COUNT}}": "Ajouter un compte via phrase secrète {{step}}/{{STEPS_COUNT}}",
|
||||
"Add an address": "Ajouter une adresse",
|
||||
"Add an existing code hash": "Ajouter un code de hachage existant",
|
||||
"Add an existing contract": "Ajouter un contrat existant",
|
||||
|
||||
@ -42,7 +42,7 @@
|
||||
"Add account": "Tambahkan akun",
|
||||
"Add account via Ledger": "Tambah akun melalui Ledger",
|
||||
"Add account via Qr": "Tambahkan akun melalui Qr",
|
||||
"Add an account via seed": "Tambahkan akun via seed",
|
||||
"Add an account via seed {{step}}/{{STEPS_COUNT}}": "Tambahkan akun via seed {{step}}/{{STEPS_COUNT}}",
|
||||
"Add an address": "Tambah sebuah alamat",
|
||||
"Add an existing code hash": "Tambahkan kode hash yang ada",
|
||||
"Add an existing contract": "Tambahkan kontrak yang ada",
|
||||
|
||||
@ -42,7 +42,7 @@
|
||||
"Add account": "Aggiungi account",
|
||||
"Add account via Ledger": "Aggiungi account via Ledger",
|
||||
"Add account via Qr": "Aggiungi account via QR Code",
|
||||
"Add an account via seed": "Aggiungi account via seed",
|
||||
"Add an account via seed {{step}}/{{STEPS_COUNT}}": "Aggiungi account via seed {{step}}/{{STEPS_COUNT}}",
|
||||
"Add an address": "Aggiungi un indirizzo",
|
||||
"Add an existing code hash": "Aggiungi un codice hash",
|
||||
"Add an existing contract": "Aggiungi un contratto",
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
"Add Ledger": "クエリーレッジャー",
|
||||
"Add account": "アカウントを追加する",
|
||||
"Add account via Qr": "QRを追加する",
|
||||
"Add an account via seed": "シード経由でアカウントを追加する",
|
||||
"Add an account via seed {{step}}/{{STEPS_COUNT}}": "シード経由でアカウントを追加する{{step}}/{{STEPS_COUNT}}",
|
||||
"Add an address": "アドレスを追加する",
|
||||
"Add an existing code hash": "既存のコードハッシュを追加する",
|
||||
"Add an existing contract": "既存のコントラクトを追加する",
|
||||
|
||||
@ -41,7 +41,7 @@
|
||||
"Add account": "계정 추가하기",
|
||||
"Add account via Ledger": "원장 통해서 계정을 추가하다",
|
||||
"Add account via Qr": "QR코드로 계정 추가하기",
|
||||
"Add an account via seed": "난수 값을 이용해서 계정 생성하기",
|
||||
"Add an account via seed {{step}}/{{STEPS_COUNT}}": "난수 값을 이용해서 계정 생성하기{{step}}/{{STEPS_COUNT}}",
|
||||
"Add an address": "주소 추가하기",
|
||||
"Add an existing code hash": "현재 존재하는 코드 해시를 추가하다",
|
||||
"Add an existing contract": "현재 존재하는 계약서를 추가하다",
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
"Add a tip to this extrinsic, paying the block author for greater priority": "Adicione uma gorjeta a esse extrínseco, pagando ao autor do bloco por maior prioridade",
|
||||
"Add account": "Adicionar Conta",
|
||||
"Add account via Qr": "Adicionar conta via código QR",
|
||||
"Add an account via seed": "Adicionar uma conta via semente",
|
||||
"Add an account via seed {{step}}/{{STEPS_COUNT}}": "Adicionar uma conta via semente {{step}}/{{STEPS_COUNT}}",
|
||||
"Add an address": "Adicionar um endereço",
|
||||
"Add an existing code hash": "Adicione um hash de código existente",
|
||||
"Add an existing contract": "Adicionar um contrato existente",
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
"Add a tip to this extrinsic, paying the block author for greater priority": "Добавыть чаевые для автора блока для большей приоритезации",
|
||||
"Add account": "Добавить аккаунт",
|
||||
"Add account via Qr": "Добавить аккаунт с помощью QR-кода",
|
||||
"Add an account via seed": "Добавить аккаунт с помощью секретной фразы",
|
||||
"Add an account via seed {{step}}/{{STEPS_COUNT}}": "Добавить аккаунт с помощью секретной фразы {{step}}/{{STEPS_COUNT}}",
|
||||
"Add an address": "Добавить адрес",
|
||||
"Add an existing code hash": "Добавить существующий хэш кода",
|
||||
"Add an existing contract": "Добавить существующий контракт",
|
||||
|
||||
@ -39,7 +39,7 @@
|
||||
"Add a tip to this extrinsic, paying the block author for greater priority": "添加小费到这个外部,为获得更高的优先级去支付区块生产者",
|
||||
"Add account": "添加账户",
|
||||
"Add account via Qr": "通过二维码添加账户",
|
||||
"Add an account via seed": "通过种子添加账户",
|
||||
"Add an account via seed {{step}}/{{STEPS_COUNT}}": "通过种子添加账户 {{step}}/{{STEPS_COUNT}}",
|
||||
"Add an address": "添加一个地址",
|
||||
"Add an existing code hash": "添加一个已经存在的代码哈希",
|
||||
"Add an existing contract": "添加一个已存在的合约",
|
||||
@ -1380,4 +1380,4 @@
|
||||
"{{threshold}}, not passing": "{{threshold}},没有通过",
|
||||
"{{threshold}}, passing": "{{threshold}}, 通过",
|
||||
"{{value}}x voting balance, locked for {{lock}}x enactment ({{period}} days)": "{{value}}x投票余额,锁定 {{lock}}x 执行 ({{period}} days)"
|
||||
}
|
||||
}
|
||||
|
||||
@ -16,6 +16,7 @@ export const darkTheme: ThemeDef = {
|
||||
borderTable: 'transparent',
|
||||
borderTabs: 'transparent',
|
||||
color: 'rgba(244, 242, 240, 0.9)',
|
||||
colorCheckbox: 'rgba(78, 78, 78, 0.66)',
|
||||
colorError: 'rgba(255, 0, 0, 0.85)',
|
||||
colorLabel: 'rgba(244, 242, 240, 0.45)',
|
||||
colorSummary: 'rgba(244, 242, 240, 0.75)',
|
||||
@ -35,6 +36,7 @@ export const lightTheme: ThemeDef = {
|
||||
borderTable: '#eeecea',
|
||||
borderTabs: '#eeecea',
|
||||
color: '#4e4e4e',
|
||||
colorCheckbox: 'rgba(34, 36, 38, 0.15)',
|
||||
colorError: 'rgba(139, 0, 0)',
|
||||
colorLabel: 'rgba(78, 78, 78, 0.66)',
|
||||
colorSummary: 'rgba(0, 0, 0, 0.6)',
|
||||
|
||||
@ -3,12 +3,14 @@
|
||||
|
||||
import AccountsApp from '@polkadot/app-accounts';
|
||||
import { MemoryStore } from '@polkadot/app-accounts/test-support/MemoryStore';
|
||||
import { lightTheme } from '@polkadot/apps/themes';
|
||||
import { Api } from '@polkadot/react-api';
|
||||
import '@polkadot/react-components/i18n';
|
||||
import { useApi } from '@polkadot/react-hooks';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { fireEvent, render, waitForElementToBeRemoved } from '@testing-library/react';
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
|
||||
const SUBSTRATE_PORT = Number.parseInt(process.env.TEST_SUBSTRATE_PORT || '30333');
|
||||
|
||||
@ -18,12 +20,12 @@ const WaitForApi = ({ children }: { children: React.ReactNode }): PropsWithChild
|
||||
return api.isApiReady ? (children) : null;
|
||||
};
|
||||
|
||||
describe('--SLOW--: Account Create', () => {
|
||||
it('asks for confirmation after saving new account', async () => {
|
||||
const memoryStore = new MemoryStore();
|
||||
const renderAccounts = () => {
|
||||
const memoryStore = new MemoryStore();
|
||||
|
||||
const { findByPlaceholderText, findByTestId, findByText } = render(
|
||||
<MemoryRouter>
|
||||
return render(
|
||||
<MemoryRouter>
|
||||
<ThemeProvider theme={lightTheme}>
|
||||
<Api store={memoryStore}
|
||||
url={`ws://127.0.0.1:${SUBSTRATE_PORT}`}>
|
||||
<WaitForApi>
|
||||
@ -34,29 +36,70 @@ describe('--SLOW--: Account Create', () => {
|
||||
</div>
|
||||
</WaitForApi>
|
||||
</Api>
|
||||
</MemoryRouter>
|
||||
);
|
||||
</ThemeProvider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
};
|
||||
|
||||
const addAccountButton = await findByText('Add account', {}, { timeout: 4000 });
|
||||
describe.only('--SLOW--: Account Create', () => {
|
||||
it('new create modal', async () => {
|
||||
const { findByTestId, findByText, queryByText } = renderAccounts();
|
||||
|
||||
const addAccountButton = await findByText('Add account', {}, { timeout: 5000 });
|
||||
|
||||
fireEvent.click(addAccountButton);
|
||||
|
||||
const nameInput = await findByPlaceholderText('new account');
|
||||
const isSeedSavedCheckbox = await findByText('I have saved my mnemonic seed safely');
|
||||
const hiddenCheckbox = isSeedSavedCheckbox as HTMLInputElement;
|
||||
|
||||
fireEvent.change(nameInput, { target: { value: 'my super account' } });
|
||||
fireEvent.click(hiddenCheckbox);
|
||||
|
||||
const nextStepButton = await findByText('Next', {}, { timeout: 4000 });
|
||||
|
||||
fireEvent.click(nextStepButton);
|
||||
|
||||
const accountNameInput = await findByTestId('name');
|
||||
|
||||
fireEvent.change(accountNameInput, { target: { value: 'my new account' } });
|
||||
|
||||
const passwordInput = await findByTestId('password');
|
||||
|
||||
fireEvent.change(passwordInput, { target: { value: 'a' } });
|
||||
fireEvent.change(passwordInput, { target: { value: 'password' } });
|
||||
|
||||
const passwordRepeatInput = await findByTestId('password (repeat)');
|
||||
const passwordInput2 = await findByTestId('password (repeat)');
|
||||
|
||||
fireEvent.change(passwordRepeatInput, { target: { value: 'a' } });
|
||||
fireEvent.change(passwordInput2, { target: { value: 'password' } });
|
||||
|
||||
const saveButton = await findByText('Save');
|
||||
const toStep3Button = await findByText('Next', {}, { timeout: 4000 });
|
||||
|
||||
fireEvent.click(saveButton);
|
||||
fireEvent.click(toStep3Button);
|
||||
|
||||
expect(await findByText('Create and backup account')).toBeTruthy();
|
||||
const createAnAccountButton = await findByText('Save', {}, { timeout: 4000 });
|
||||
|
||||
fireEvent.click(createAnAccountButton);
|
||||
|
||||
await waitForElementToBeRemoved(() => queryByText('Add an account via seed'));
|
||||
|
||||
expect(await findByText('MY NEW ACCOUNT')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('error message for derivation path', async () => {
|
||||
const { findByTestId, findByText } = renderAccounts();
|
||||
|
||||
const addAccountButton = await findByText('Add account', {}, { timeout: 5000 });
|
||||
|
||||
fireEvent.click(addAccountButton);
|
||||
|
||||
const showAdvancedOptionsButton = await findByText('Advanced creation options', {}, { timeout: 5000 });
|
||||
|
||||
fireEvent.click(showAdvancedOptionsButton);
|
||||
|
||||
const derivationPathInput = await findByTestId('secret derivation path', {}, { timeout: 5000 });
|
||||
|
||||
fireEvent.change(derivationPathInput, { target: { value: '//abc//' } });
|
||||
|
||||
const errorMsg = await findByText('Unable to match provided value to a secret URI', {}, { timeout: 5000 });
|
||||
|
||||
expect(errorMsg).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@ -10,8 +10,8 @@ import FileSaver from 'file-saver';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { DEV_PHRASE } from '@polkadot/keyring/defaults';
|
||||
import { AddressRow, Button, CopyButton, Dropdown, Expander, Input, InputAddress, Modal } from '@polkadot/react-components';
|
||||
import { useApi, useToggle } from '@polkadot/react-hooks';
|
||||
import { AddressRow, Button, Checkbox, CopyButton, Dropdown, Expander, Input, InputAddress, Modal, TextArea } from '@polkadot/react-components';
|
||||
import { useApi } from '@polkadot/react-hooks';
|
||||
import keyring from '@polkadot/ui-keyring';
|
||||
import uiSettings from '@polkadot/ui-settings';
|
||||
import { isHex, u8aToHex } from '@polkadot/util';
|
||||
@ -55,6 +55,7 @@ interface DeriveValidationOutput {
|
||||
}
|
||||
|
||||
const DEFAULT_PAIR_TYPE = 'sr25519';
|
||||
const STEPS_COUNT = 3;
|
||||
|
||||
function deriveValidate (seed: string, seedType: SeedType, derivePath: string, pairType: KeypairType): DeriveValidationOutput {
|
||||
try {
|
||||
@ -183,11 +184,14 @@ function Create ({ className = '', onClose, onStatusChange, seed: propsSeed, typ
|
||||
const { t } = useTranslation();
|
||||
const { api, isDevelopment } = useApi();
|
||||
const [{ address, derivePath, deriveValidation, isSeedValid, pairType, seed, seedType }, setAddress] = useState<AddressState>(generateSeed(propsSeed, '', propsSeed ? 'raw' : 'bip', propsType));
|
||||
const [isConfirmationOpen, toggleConfirmation] = useToggle();
|
||||
const [isMnemonicSaved, setIsMnemonicSaved] = useState<boolean>(false);
|
||||
const [step, setStep] = useState(1);
|
||||
const [isBusy, setIsBusy] = useState(false);
|
||||
const [{ isNameValid, name }, setName] = useState({ isNameValid: false, name: '' });
|
||||
const [{ isPasswordValid, password }, setPassword] = useState({ isPasswordValid: false, password: '' });
|
||||
const isValid = !!address && !deriveValidation?.error && isNameValid && isPasswordValid && isSeedValid;
|
||||
const isFirstStepValid = !!address && isMnemonicSaved && !deriveValidation?.error && isSeedValid;
|
||||
const isSecondStepValid = isNameValid && isPasswordValid;
|
||||
const isValid = isFirstStepValid && isSecondStepValid;
|
||||
const errorIndex: Record<string, string> = useMemo(() => ({
|
||||
PASSWORD_IGNORED: t<string>('Password are ignored for hex seed'),
|
||||
SOFT_NOT_ALLOWED: t<string>('Soft derivation paths are not allowed on ed25519'),
|
||||
@ -237,6 +241,19 @@ function Create ({ className = '', onClose, onStatusChange, seed: propsSeed, typ
|
||||
[]
|
||||
);
|
||||
|
||||
const _toggleMnemonicSaved = () => {
|
||||
setIsMnemonicSaved(!isMnemonicSaved);
|
||||
};
|
||||
|
||||
const _nextStep = useCallback(
|
||||
() => setStep((step) => step + 1),
|
||||
[]
|
||||
);
|
||||
|
||||
const _previousStep = useCallback(
|
||||
() => setStep((step) => step - 1),
|
||||
[]
|
||||
);
|
||||
const _onCommit = useCallback(
|
||||
(): void => {
|
||||
if (!isValid) {
|
||||
@ -259,150 +276,181 @@ function Create ({ className = '', onClose, onStatusChange, seed: propsSeed, typ
|
||||
return (
|
||||
<Modal
|
||||
className={className}
|
||||
header={t<string>('Add an account via seed')}
|
||||
header={t<string>('Add an account via seed {{step}}/{{STEPS_COUNT}}', { replace: { STEPS_COUNT, step } })}
|
||||
size='large'
|
||||
>
|
||||
{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}
|
||||
<Modal.Content>
|
||||
<Modal.Columns>
|
||||
<Modal.Column>
|
||||
<AddressRow
|
||||
defaultName={name}
|
||||
fullLength
|
||||
isEditableName={false}
|
||||
noDefaultNameOpacity
|
||||
value={isSeedValid ? address : ''}
|
||||
/>
|
||||
<ExternalWarning />
|
||||
<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={!!deriveValidation?.error}
|
||||
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}
|
||||
/>
|
||||
{deriveValidation?.error && (
|
||||
<article className='error'>{errorIndex[deriveValidation.error] || deriveValidation.error}</article>
|
||||
)}
|
||||
{deriveValidation?.warning && (
|
||||
<article className='warning'>{errorIndex[deriveValidation.warning]}</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.Column>
|
||||
</Modal.Columns>
|
||||
{step === 1 && <>
|
||||
<Modal.Columns>
|
||||
<Modal.Column>
|
||||
<TextArea
|
||||
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}
|
||||
seed={seed}
|
||||
withLabel
|
||||
>
|
||||
<Dropdown
|
||||
defaultValue={seedType}
|
||||
isButton
|
||||
onChange={_selectSeedType}
|
||||
options={seedOpt}
|
||||
/>
|
||||
<CopyButton
|
||||
className='copyMoved'
|
||||
isMnemonic
|
||||
value={seed}
|
||||
/>
|
||||
</TextArea>
|
||||
</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>
|
||||
<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={!!deriveValidation?.error}
|
||||
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}
|
||||
/>
|
||||
{deriveValidation?.error && (
|
||||
<article className='error'>{errorIndex[deriveValidation.error] || deriveValidation.error}</article>
|
||||
)}
|
||||
{deriveValidation?.warning && (
|
||||
<article className='warning'>{errorIndex[deriveValidation.warning]}</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>
|
||||
<ExternalWarning />
|
||||
<Modal.Columns>
|
||||
<Modal.Column> </Modal.Column>
|
||||
<Modal.Column>
|
||||
<Checkbox
|
||||
label={<>{t<string>('I have saved my mnemonic seed safely')}</>}
|
||||
onChange={_toggleMnemonicSaved}
|
||||
value={isMnemonicSaved}
|
||||
/>
|
||||
</Modal.Column>
|
||||
</Modal.Columns>
|
||||
</>}
|
||||
{step === 2 && <>
|
||||
<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>
|
||||
<PasswordInput
|
||||
onChange={_onPasswordChange}
|
||||
onEnter={_onCommit}
|
||||
/>
|
||||
<ExternalWarning />
|
||||
</>}
|
||||
{step === 3 && address && <CreateConfirmation
|
||||
derivePath={derivePath}
|
||||
isBusy={isBusy}
|
||||
pairType={pairType}
|
||||
seed={seed}
|
||||
/>
|
||||
}
|
||||
</Modal.Content>
|
||||
<Modal.Actions onCancel={onClose}>
|
||||
{isConfirmationOpen
|
||||
? (
|
||||
{step === 1 &&
|
||||
<Button
|
||||
icon='step-forward'
|
||||
isDisabled={!isFirstStepValid}
|
||||
label={t<string>('Next')}
|
||||
onClick={_nextStep}
|
||||
/>
|
||||
}
|
||||
{step === 2 &&
|
||||
<><Button
|
||||
icon='step-backward'
|
||||
label={t<string>('Prev')}
|
||||
onClick={_previousStep}
|
||||
/>
|
||||
<Button
|
||||
icon='step-forward'
|
||||
isDisabled={!isSecondStepValid}
|
||||
label={t<string>('Next')}
|
||||
onClick={_nextStep}
|
||||
/>
|
||||
</>}
|
||||
{step === 3 &&
|
||||
<>
|
||||
<Button
|
||||
icon='step-backward'
|
||||
label={t<string>('Prev')}
|
||||
onClick={toggleConfirmation}
|
||||
onClick={_previousStep}
|
||||
/>
|
||||
<Button
|
||||
icon='plus'
|
||||
@ -411,15 +459,6 @@ function Create ({ className = '', onClose, onStatusChange, seed: propsSeed, typ
|
||||
onClick={_onCommit}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
: (
|
||||
<Button
|
||||
icon='step-forward'
|
||||
isDisabled={!isValid}
|
||||
label={t<string>('Next')}
|
||||
onClick={toggleConfirmation}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</Modal.Actions>
|
||||
</Modal>
|
||||
@ -434,7 +473,22 @@ export default React.memo(styled(Create)`
|
||||
|
||||
.copyMoved {
|
||||
position: absolute;
|
||||
right: 8rem;
|
||||
top: 0.75rem;
|
||||
right: 9.1rem;
|
||||
top: 1.25rem;
|
||||
}
|
||||
&& .TextAreaWithDropdown {
|
||||
textarea {
|
||||
width: 80%;
|
||||
}
|
||||
.ui.buttons {
|
||||
width: 20%;
|
||||
}
|
||||
}
|
||||
& .ui--Checkbox {
|
||||
margin: 0.8rem 0 0 2rem;
|
||||
|
||||
> label {
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
`);
|
||||
|
||||
@ -10,10 +10,10 @@ import { isHex } from '@polkadot/util';
|
||||
import { useTranslation } from '../translate';
|
||||
|
||||
interface Props {
|
||||
address: string;
|
||||
address?: string;
|
||||
derivePath: string;
|
||||
isBusy: boolean;
|
||||
name: string;
|
||||
name?: string;
|
||||
pairType: KeypairType;
|
||||
seed?: string;
|
||||
}
|
||||
@ -30,12 +30,12 @@ function CreateConfirmation ({ address, derivePath, name, pairType, seed }: Prop
|
||||
<Modal.Content>
|
||||
<Modal.Columns>
|
||||
<Modal.Column>
|
||||
<AddressRow
|
||||
{address && name && <AddressRow
|
||||
defaultName={name}
|
||||
isInline
|
||||
noDefaultNameOpacity
|
||||
value={address}
|
||||
/>
|
||||
/>}
|
||||
{shortSeed && (
|
||||
<Static
|
||||
label={t<string>('partial seed')}
|
||||
|
||||
@ -23,11 +23,17 @@ export default function PasswordInput ({ onChange, onEnter }: Props): React.Reac
|
||||
);
|
||||
|
||||
const _onPassword1Change = useCallback(
|
||||
(password1: string) => setPassword1({
|
||||
isPass1Valid: keyring.isPassValid(password1),
|
||||
password1
|
||||
}),
|
||||
[]
|
||||
(password1: string) => {
|
||||
setPassword1({
|
||||
isPass1Valid: keyring.isPassValid(password1),
|
||||
password1
|
||||
});
|
||||
setPassword2({
|
||||
isPass2Valid: keyring.isPassValid(password2) && (password2 === password1),
|
||||
password2
|
||||
});
|
||||
},
|
||||
[password2]
|
||||
);
|
||||
|
||||
const onPassword2Change = useCallback(
|
||||
|
||||
@ -58,6 +58,14 @@ let api: ApiPromise;
|
||||
|
||||
export { api };
|
||||
|
||||
function isKeyringLoaded () {
|
||||
try {
|
||||
return !!keyring.keyring;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function getDevTypes (): Record<string, Record<string, string>> {
|
||||
const types = store.get('types', {}) as Record<string, Record<string, string>>;
|
||||
const names = Object.keys(types);
|
||||
@ -138,7 +146,7 @@ async function loadOnReady (api: ApiPromise, injectedPromise: Promise<InjectedEx
|
||||
TokenUnit.setAbbr(tokenSymbol);
|
||||
|
||||
// finally load the keyring
|
||||
keyring.loadAll({
|
||||
isKeyringLoaded() || keyring.loadAll({
|
||||
genesisHash: api.genesisHash,
|
||||
isDevelopment,
|
||||
ss58Format,
|
||||
|
||||
@ -27,6 +27,7 @@
|
||||
"codeflask": "^1.4.1",
|
||||
"i18next": "^19.8.2",
|
||||
"i18next-browser-languagedetector": "^5.0.1",
|
||||
"owasp-password-strength-test": "^1.3.0",
|
||||
"react": "^16.14.0",
|
||||
"react-beautiful-dnd": "^13.0.0",
|
||||
"react-chartjs-2": "^2.10.0",
|
||||
@ -46,6 +47,7 @@
|
||||
"styled-theming": "^2.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/codeflask": "^1.4.2"
|
||||
"@types/codeflask": "^1.4.2",
|
||||
"@types/owasp-password-strength-test": "^1.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,6 +15,7 @@ import Row, { RowProps } from './Row';
|
||||
export interface Props extends RowProps {
|
||||
isContract?: boolean;
|
||||
isValid?: boolean;
|
||||
fullLength?: boolean;
|
||||
label?: string;
|
||||
noDefaultNameOpacity?: boolean;
|
||||
overlay?: React.ReactNode;
|
||||
@ -26,15 +27,16 @@ export interface Props extends RowProps {
|
||||
const DEFAULT_ADDR = '5'.padEnd(48, 'x');
|
||||
const ICON_SIZE = 32;
|
||||
|
||||
function AddressRow ({ buttons, children, className, defaultName, isContract = false, isDisabled, isInline, isValid: propsIsValid, overlay, value, withTags = false }: Props): React.ReactElement<Props> | null {
|
||||
function AddressRow ({ buttons, children, className, defaultName, fullLength = false, isContract = false, isDisabled, isEditableName, isInline, isValid: propsIsValid, overlay, value, withTags = false }: Props): React.ReactElement<Props> | null {
|
||||
const { accountIndex, isNull, name, onSaveName, onSaveTags, setName, setTags, tags } = useAccountInfo(value ? value.toString() : null, isContract);
|
||||
|
||||
const isValid = !isNull && (propsIsValid || value || accountIndex);
|
||||
const Icon = value ? IdentityIcon : BaseIdentityIcon;
|
||||
const address = value && isValid ? value : DEFAULT_ADDR;
|
||||
|
||||
return (
|
||||
<Row
|
||||
address={toShortAddress(value && isValid ? value : DEFAULT_ADDR)}
|
||||
address={fullLength ? address : toShortAddress(address)}
|
||||
buttons={buttons}
|
||||
className={className}
|
||||
defaultName={defaultName}
|
||||
@ -45,7 +47,7 @@ function AddressRow ({ buttons, children, className, defaultName, isContract = f
|
||||
/>
|
||||
}
|
||||
isDisabled={isDisabled}
|
||||
isEditableName
|
||||
isEditableName={isEditableName}
|
||||
isEditableTags
|
||||
isInline={isInline}
|
||||
name={name}
|
||||
@ -68,7 +70,7 @@ export {
|
||||
|
||||
export default React.memo(styled(AddressRow)`
|
||||
button.u.ui--Icon.editButton {
|
||||
padding: 0em .3em .3em .3em;
|
||||
padding: 0 .3em .3em .3em;
|
||||
color: #2e86ab;
|
||||
background: none;
|
||||
/*trick to let the button in the flow but keep the content centered regardless*/
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { ThemeProps } from './types';
|
||||
import Icon from './Icon';
|
||||
|
||||
interface Props {
|
||||
@ -23,18 +24,23 @@ function Checkbox ({ className = '', isDisabled, label, onChange, value }: Props
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`ui--Checkbox${isDisabled ? ' isDisabled' : ''} ${className}`}>
|
||||
<div
|
||||
className={`ui--Checkbox${isDisabled ? ' isDisabled' : ''} ${className}`}
|
||||
onClick={_onClick}
|
||||
>
|
||||
<Icon
|
||||
color={value ? 'normal' : 'transparent'}
|
||||
icon='check'
|
||||
onClick={_onClick}
|
||||
/>
|
||||
{label && <label>{label}</label>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(styled(Checkbox)`
|
||||
export default React.memo(styled(Checkbox)(({ theme }: ThemeProps) => `
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
|
||||
&.isDisabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
@ -44,10 +50,12 @@ export default React.memo(styled(Checkbox)`
|
||||
}
|
||||
|
||||
> label {
|
||||
color: rgba(78, 78, 78, 0.75);
|
||||
color: ${theme.color};
|
||||
display: inline-block;
|
||||
margin: 0 0.5rem;
|
||||
opacity: 1;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
> label,
|
||||
@ -56,7 +64,7 @@ export default React.memo(styled(Checkbox)`
|
||||
}
|
||||
|
||||
.ui--Icon {
|
||||
border: 1px solid rgba(34, 36, 38, 0.15);
|
||||
border: 1px solid ${theme.colorCheckbox};
|
||||
border-radius: 0.125rem;
|
||||
}
|
||||
`);
|
||||
`));
|
||||
|
||||
@ -16,25 +16,26 @@ interface Props {
|
||||
className?: string;
|
||||
icon?: IconName;
|
||||
isAddress?: boolean;
|
||||
isMnemonic?: boolean;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const NOOP = () => undefined;
|
||||
|
||||
function CopyButton ({ children, className, icon = 'copy', isAddress = false, value }: Props): React.ReactElement<Props> {
|
||||
function CopyButton ({ children, className, icon = 'copy', isAddress = false, isMnemonic = false, value }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const { queueAction } = useContext(StatusContext);
|
||||
|
||||
const _onCopy = useCallback(
|
||||
(): void => {
|
||||
isAddress && queueAction && queueAction({
|
||||
account: value,
|
||||
(isAddress || isMnemonic) && queueAction && queueAction({
|
||||
account: isAddress ? value : undefined,
|
||||
action: t<string>('clipboard'),
|
||||
message: t<string>('address copied'),
|
||||
message: t<string>(`${isAddress ? 'address' : 'mnemonic'} copied`),
|
||||
status: 'queued'
|
||||
});
|
||||
},
|
||||
[isAddress, queueAction, t, value]
|
||||
[isAddress, isMnemonic, queueAction, t, value]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@ -1,8 +1,22 @@
|
||||
// Copyright 2017-2020 @polkadot/react-components authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import { ThemeProps } from './types';
|
||||
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import strengthTester from 'owasp-password-strength-test';
|
||||
|
||||
import { useTranslation } from './translate';
|
||||
|
||||
const MAX_STRENGTH = 7; // equal to number of password tests in owasp strength tester
|
||||
|
||||
strengthTester.config({
|
||||
allowPassphrases: true,
|
||||
maxLength: 128,
|
||||
minLength: 8,
|
||||
minPhraseLength: 20
|
||||
});
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
@ -10,43 +24,57 @@ interface Props {
|
||||
}
|
||||
|
||||
function calcStrength (password: string): number {
|
||||
// at least 8 characters
|
||||
return (/.{8,}/.test(password) ? 1 : 0) * (
|
||||
// bonus if longer
|
||||
(/.{12,}/.test(password) ? 1 : 0) +
|
||||
// a lower letter
|
||||
(/[a-z]/.test(password) ? 1 : 0) +
|
||||
// a upper letter
|
||||
(/[A-Z]/.test(password) ? 1 : 0) +
|
||||
// a digit
|
||||
(/\d/.test(password) ? 1 : 0) +
|
||||
// a special character
|
||||
(/[^A-Za-z0-9]/.test(password) ? 1 : 0)
|
||||
);
|
||||
const testResult = strengthTester.test(password);
|
||||
const passedTests = Math.max(0, testResult.passedTests.length - testResult.failedTests.length);
|
||||
|
||||
return testResult.isPassphrase ? MAX_STRENGTH : passedTests;
|
||||
}
|
||||
|
||||
function PasswordStrength ({ className = '', value }: Props): React.ReactElement<Props> {
|
||||
// No need for memo, component is already memo-ed (only changes on value)
|
||||
const style = { width: `${calcStrength(value) * 20}%` };
|
||||
const { t } = useTranslation();
|
||||
|
||||
const passwordStrength = calcStrength(value);
|
||||
const style = { width: `${passwordStrength * 100 / MAX_STRENGTH}%` };
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div
|
||||
className='highlight--bg'
|
||||
style={style}
|
||||
/>
|
||||
<div
|
||||
className={className}
|
||||
style={{ display: (value ? 'flex' : 'none') }}
|
||||
>
|
||||
{t<string>('weak')}
|
||||
<div className='ui--Strength-bar'>
|
||||
<div
|
||||
className='ui--Strength-bar-highlighted'
|
||||
style={style}
|
||||
/>
|
||||
</div>
|
||||
{t<string>('strong')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(styled(PasswordStrength)`
|
||||
background: ##eee6e6;
|
||||
border-radius: 0.25rem;
|
||||
export default React.memo(styled(PasswordStrength)(({ theme }: ThemeProps) => `
|
||||
align-items: center;
|
||||
margin-top: 0.5rem;
|
||||
margin-left: 2rem;
|
||||
margin-top: 0.25rem;
|
||||
overflow: hidden;
|
||||
|
||||
> div {
|
||||
height: 0.5rem;
|
||||
font-size: 1rem;
|
||||
text-transform: uppercase;
|
||||
color: ${theme.colorLabel};
|
||||
.ui--Strength-bar {
|
||||
position: relative;
|
||||
height: 0.6rem;
|
||||
width: 100%;
|
||||
margin: 0 10px;
|
||||
border: 1px solid #DFDFDF;
|
||||
border-radius: 0.15rem;
|
||||
background: #ECECEC;
|
||||
}
|
||||
`);
|
||||
.ui--Strength-bar-highlighted {
|
||||
position: absolute;
|
||||
top: -0.07rem;
|
||||
height: 0.6rem;
|
||||
width: 100%;
|
||||
border-radius: 0.15rem;
|
||||
background: linear-gradient(90.43deg, #FF8B00 0%, #FFBB50 112.75%);
|
||||
}
|
||||
`));
|
||||
|
||||
@ -214,7 +214,7 @@ function Row ({ address, buttons, children, className, defaultName, details, ico
|
||||
{name || defaultName}
|
||||
</EditButton>
|
||||
)
|
||||
: name
|
||||
: name || defaultName
|
||||
}
|
||||
</div>
|
||||
)
|
||||
|
||||
100
packages/react-components/src/TextArea.tsx
Normal file
100
packages/react-components/src/TextArea.tsx
Normal file
@ -0,0 +1,100 @@
|
||||
// Copyright 2017-2020 @polkadot/react-components authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React, { useCallback } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { Labelled } from '.';
|
||||
import { ThemeProps } from './types';
|
||||
|
||||
interface Props {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
help?: React.ReactNode;
|
||||
isError?: boolean;
|
||||
isReadOnly?: boolean;
|
||||
label?: React.ReactNode;
|
||||
onChange?: (arg: string) => void;
|
||||
seed?: string;
|
||||
withLabel?: boolean;
|
||||
}
|
||||
|
||||
function TextArea ({ children, className, help, isError, isReadOnly, label, onChange, seed, withLabel }: Props): React.ReactElement<Props> {
|
||||
const _onChange = useCallback(
|
||||
({ target: { value } }: React.ChangeEvent<HTMLTextAreaElement>): void => {
|
||||
onChange && onChange(value);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<Labelled
|
||||
className={className}
|
||||
help={help}
|
||||
label={label}
|
||||
withLabel={withLabel}
|
||||
>
|
||||
<div className='TextAreaWithDropdown'>
|
||||
<textarea
|
||||
autoCapitalize='off'
|
||||
autoCorrect='off'
|
||||
autoFocus={false}
|
||||
className={isError ? 'ui-textArea-withError' : ''}
|
||||
onChange={_onChange}
|
||||
readOnly={isReadOnly}
|
||||
rows={2}
|
||||
spellCheck={false}
|
||||
value={seed}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
</Labelled>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(styled(TextArea)(({ theme }: ThemeProps) => `
|
||||
.TextAreaWithDropdown {
|
||||
display: flex;
|
||||
textarea {
|
||||
border-radius: 0.25rem 0 0 0.25rem;
|
||||
border: 1px solid #DDE1EB;
|
||||
border-right: none;
|
||||
background: ${theme.bgInput};
|
||||
box-sizing: border-box;
|
||||
color: ${theme.color};
|
||||
display: block;
|
||||
outline: none;
|
||||
padding: 1.75rem 3rem 0.75rem 1.5rem;
|
||||
resize: none;
|
||||
width: 100%;
|
||||
|
||||
&:read-only {
|
||||
background: ${theme.bgInverse};
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
|
||||
~ .ui.buttons > .ui.selection.dropdown {
|
||||
background: ${theme.bgInverse};
|
||||
}
|
||||
}
|
||||
|
||||
&.ui-textArea-withError {
|
||||
background: ${theme.bgInputError};
|
||||
color: ${theme.colorError};
|
||||
}
|
||||
}
|
||||
|
||||
& > .ui.buttons > .ui.button.floating.selection.dropdown {
|
||||
border: 1px solid #DDE1EB;
|
||||
border-left: none;
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
& > .dropdown.icon {
|
||||
top: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
`));
|
||||
@ -97,5 +97,6 @@ export { default as TxModal } from './TxModal';
|
||||
export { default as TxModalNew } from './TxModalNew';
|
||||
export { default as VoteAccount } from './VoteAccount';
|
||||
export { default as VoteValue } from './VoteValue';
|
||||
export { default as TextArea } from './TextArea';
|
||||
|
||||
export { default as media } from './media';
|
||||
|
||||
@ -138,6 +138,7 @@ export interface ThemeDef {
|
||||
borderTable: string;
|
||||
borderTabs: string;
|
||||
color: string;
|
||||
colorCheckbox: string;
|
||||
colorError: string;
|
||||
colorLabel: string;
|
||||
colorSummary: string;
|
||||
|
||||
16
yarn.lock
16
yarn.lock
@ -2638,10 +2638,12 @@ __metadata:
|
||||
"@polkadot/util": ^3.6.1
|
||||
"@polkadot/util-crypto": ^3.6.1
|
||||
"@types/codeflask": ^1.4.2
|
||||
"@types/owasp-password-strength-test": ^1.3.0
|
||||
chart.js: ^2.9.3
|
||||
codeflask: ^1.4.1
|
||||
i18next: ^19.8.2
|
||||
i18next-browser-languagedetector: ^5.0.1
|
||||
owasp-password-strength-test: ^1.3.0
|
||||
react: ^16.14.0
|
||||
react-beautiful-dnd: ^13.0.0
|
||||
react-chartjs-2: ^2.10.0
|
||||
@ -3503,6 +3505,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/owasp-password-strength-test@npm:^1.3.0":
|
||||
version: 1.3.0
|
||||
resolution: "@types/owasp-password-strength-test@npm:1.3.0"
|
||||
checksum: 0650d0bde06513692bca073c997d5d37c52ac6a60390761a05d3dc57cb23ed22c36c6f847eb58de004aeee995d780ec270931ae3cdbc69865941c02ca1dd5981
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/parse-json@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "@types/parse-json@npm:4.0.0"
|
||||
@ -13785,6 +13794,13 @@ fsevents@^1.2.7:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"owasp-password-strength-test@npm:^1.3.0":
|
||||
version: 1.3.0
|
||||
resolution: "owasp-password-strength-test@npm:1.3.0"
|
||||
checksum: beb2e677826e1e3b6fd29d5c75ed89bb6fe231b855a39537f8dbd241dd97ae91e83d044290c4b167f1e8495b3b99e984f73aa6ba30b3103e2497d4c118a29f3b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"p-cancelable@npm:^1.0.0":
|
||||
version: 1.1.0
|
||||
resolution: "p-cancelable@npm:1.1.0"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user