Refactor seed import and add explicit derivation path field (#619)
* refactor import from seed * stray import * clean up duplicates * remove unneeded global suri * wip use AccountNamePassword component * AccountNamePasswordCreation component and tests * use in Derive and enhance test * extract SeedAndPath in dedicated component * fix disabled button * pass address * keep address in component
This commit is contained in:
parent
a39d3ba696
commit
967b69ffde
@ -9,7 +9,7 @@ import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { ThemeProvider } from 'styled-components';
|
||||
|
||||
import { ActionContext, ActionText, Button, Input, InputWithLabel, themes } from '../../components';
|
||||
import { ActionContext, ActionText, Button, themes } from '../../components';
|
||||
import * as messaging from '../../messaging';
|
||||
import { Header } from '../../partials';
|
||||
import { flushAllPromises } from '../../testHelpers';
|
||||
@ -42,12 +42,6 @@ describe('Create Account', () => {
|
||||
wrapper.update();
|
||||
};
|
||||
|
||||
const capsLockOn = async (input: ReactWrapper): Promise<void> => {
|
||||
input.simulate('keyPress', { getModifierState: () => true });
|
||||
await act(flushAllPromises);
|
||||
wrapper.update();
|
||||
};
|
||||
|
||||
const enterName = (name: string): Promise<void> => type(wrapper.find('input').first(), name);
|
||||
const password = (password: string) => (): Promise<void> => type(wrapper.find('input[type="password"]').first(), password);
|
||||
const repeat = (password: string) => (): Promise<void> => type(wrapper.find('input[type="password"]').last(), password);
|
||||
@ -100,77 +94,6 @@ describe('Create Account', () => {
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
it('only account name input is visible at first', () => {
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-name]').find(Input)).toHaveLength(1);
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-password]').find(Input)).toHaveLength(1);
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-repeat-password]')).toHaveLength(0);
|
||||
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('input should not be highlighted as error until first interaction', () => {
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-name]').find(Input).prop('withError')).toBe(false);
|
||||
});
|
||||
|
||||
it('after typing less than 3 characters into name input, password input is not visible', async () => {
|
||||
await enterName('ab');
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-name]').find(Input).prop('withError')).toBe(true);
|
||||
expect(wrapper.find('.warning-message').first().text()).toBe('Account name is too short');
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-password]').find(Input)).toHaveLength(1);
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-repeat-password]')).toHaveLength(0);
|
||||
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('input should keep showing error when something has been typed but then erased', async () => {
|
||||
await enterName('ab');
|
||||
await enterName('');
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-name]').find(Input).prop('withError')).toBe(true);
|
||||
});
|
||||
|
||||
it('after typing 3 characters into name input, the name is reflected in the account card', async () => {
|
||||
const NAME = 'abc';
|
||||
|
||||
await enterName(NAME);
|
||||
expect(wrapper.find('[data-field="name"]').first().text()).toBe(NAME);
|
||||
});
|
||||
|
||||
it('after typing 3 characters into name input, first password input is visible', async () => {
|
||||
await enterName('abc');
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-name]').find(Input).first().prop('withError')).toBe(false);
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-password]').find(Input)).toHaveLength(1);
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-repeat-password]')).toHaveLength(0);
|
||||
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('password with caps lock should show a warning', async () => {
|
||||
await enterName('abc').then(password('abcde'));
|
||||
await capsLockOn(wrapper.find(InputWithLabel).find('[data-input-password]').find(Input));
|
||||
|
||||
expect(wrapper.find('.warning-message').first().text()).toBe('Warning: Caps lock is on');
|
||||
});
|
||||
|
||||
it('password shorter than 6 characters should be not valid', async () => {
|
||||
await enterName('abc').then(password('abcde'));
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-password]').find(Input).prop('withError')).toBe(true);
|
||||
expect(wrapper.find('.warning-message').text()).toBe('Password is too short');
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-password]').find(Input)).toHaveLength(1);
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-repeat-password]')).toHaveLength(0);
|
||||
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('submit button is not enabled until both passwords are equal', async () => {
|
||||
await enterName('abc').then(password('abcdef')).then(repeat('abcdeg'));
|
||||
expect(wrapper.find('.warning-message').text()).toBe('Passwords do not match');
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-repeat-password]').find(Input).prop('withError')).toBe(true);
|
||||
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('submit button is enabled when both passwords are equal', async () => {
|
||||
await enterName('abc').then(password('abcdef')).then(repeat('abcdef'));
|
||||
expect(wrapper.find('.warning-message')).toHaveLength(0);
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-repeat-password]').find(Input).prop('withError')).toBe(false);
|
||||
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(false);
|
||||
});
|
||||
|
||||
it('saves account with provided name and password', async () => {
|
||||
await enterName('abc').then(password('abcdef')).then(repeat('abcdef'));
|
||||
wrapper.find('[data-button-action="add new root"] button').simulate('click');
|
||||
@ -180,37 +103,4 @@ describe('Create Account', () => {
|
||||
expect(onActionStub).toBeCalledWith('/');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Both passwords are equal, but then', () => {
|
||||
beforeEach(async () => {
|
||||
check(wrapper.find('input[type="checkbox"]'));
|
||||
wrapper.find('button').simulate('click');
|
||||
await act(flushAllPromises);
|
||||
await enterName('abc').then(password('abcdef')).then(repeat('abcdef'));
|
||||
});
|
||||
|
||||
it('first password input is cleared - second one disappears and button get disabled', async () => {
|
||||
await type(wrapper.find('input[type="password"]').first(), '');
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-repeat-password]')).toHaveLength(0);
|
||||
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('first password changes - button is disabled', async () => {
|
||||
await type(wrapper.find('input[type="password"]').first(), 'aaaaaa');
|
||||
expect(wrapper.find('.warning-message').text()).toBe('Passwords do not match');
|
||||
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('first password changes, then second changes too - button is enabled', async () => {
|
||||
await type(wrapper.find('input[type="password"]').first(), 'aaaaaa');
|
||||
await type(wrapper.find('input[type="password"]').last(), 'aaaaaa');
|
||||
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(false);
|
||||
});
|
||||
|
||||
it('second password changes, then first changes too - button is enabled', async () => {
|
||||
await type(wrapper.find('input[type="password"]').last(), 'aaaaaa');
|
||||
await type(wrapper.find('input[type="password"]').first(), 'aaaaaa');
|
||||
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -4,11 +4,11 @@
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
|
||||
import { ActionContext, Address, Loading } from '../../components';
|
||||
import AccountNamePasswordCreation from '../../components/AccountNamePasswordCreation';
|
||||
import useTranslation from '../../hooks/useTranslation';
|
||||
import { createAccountSuri, createSeed } from '../../messaging';
|
||||
import { HeaderWithSteps } from '../../partials';
|
||||
import { DEFAULT_TYPE } from '../../util/defaultType';
|
||||
import AccountName from './AccountName';
|
||||
import Mnemonic from './Mnemonic';
|
||||
|
||||
export default function CreateAccount (): React.ReactElement {
|
||||
@ -26,7 +26,6 @@ export default function CreateAccount (): React.ReactElement {
|
||||
.catch((error: Error) => console.error(error));
|
||||
}, [type]);
|
||||
|
||||
// FIXME Duplicated between here and Import.tsx
|
||||
const _onCreate = useCallback(
|
||||
(name: string, password: string): void => {
|
||||
// this should always be the case
|
||||
@ -69,8 +68,8 @@ export default function CreateAccount (): React.ReactElement {
|
||||
/>
|
||||
)
|
||||
: (
|
||||
<AccountName
|
||||
address={account.address}
|
||||
<AccountNamePasswordCreation
|
||||
buttonLabel={t<string>('Add the account with the generated seed')}
|
||||
isBusy={isBusy}
|
||||
onBackClick={_onPreviousStep}
|
||||
onCreate={_onCreate}
|
||||
|
||||
@ -22,9 +22,14 @@ import Derive from '.';
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-call
|
||||
configure({ adapter: new Adapter() });
|
||||
|
||||
const parentPassword = 'pass';
|
||||
const westendGenesis = '0xe143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e';
|
||||
const defaultDerivation = '//0';
|
||||
const derivedAddress = '5GYQRJj3NUznYDzCduENRcocMsyxmb6tjb5xW87ZMErBe9R7';
|
||||
|
||||
const accounts = [
|
||||
{ address: '5FjgD3Ns2UpnHJPVeRViMhCttuemaRXEqaD8V5z4vxcsUByA', name: 'A', type: 'sr25519' },
|
||||
{ address: '5GYmFzQCuC5u3tQNiMZNbFGakrz3Jq31NmMg4D2QAkSoQ2g5', name: 'B', type: 'sr25519' },
|
||||
{ address: '5GYmFzQCuC5u3tQNiMZNbFGakrz3Jq31NmMg4D2QAkSoQ2g5', genesisHash: westendGenesis, name: 'B', type: 'sr25519' },
|
||||
{ address: '5D2TPhGEy2FhznvzaNYW9AkuMBbg3cyRemnPsBvBY4ZhkZXA', name: 'BB', parentAddress: '5GYmFzQCuC5u3tQNiMZNbFGakrz3Jq31NmMg4D2QAkSoQ2g5', type: 'sr25519' },
|
||||
{ address: '5GhGENSJBWQZ8d8mARKgqEkiAxiW3hHeznQDW2iG4XzNieb6', isExternal: true, name: 'C', type: 'sr25519' },
|
||||
{ address: '0xd5D81CD4236a43F48A983fc5B895975c511f634D', name: 'Ethereum', type: 'ethereum' },
|
||||
@ -70,6 +75,10 @@ describe('Derive', () => {
|
||||
input.update();
|
||||
};
|
||||
|
||||
const enterName = (name: string): Promise<void> => type(wrapper.find('input').first(), name);
|
||||
const password = (password: string) => (): Promise<void> => type(wrapper.find('input[type="password"]').first(), password);
|
||||
const repeat = (password: string) => (): Promise<void> => type(wrapper.find('input[type="password"]').last(), password);
|
||||
|
||||
describe('Parent selection screen', () => {
|
||||
beforeEach(async () => {
|
||||
const mountedComponent = await mountComponent();
|
||||
@ -79,14 +88,14 @@ describe('Derive', () => {
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
jest.spyOn(messaging, 'validateAccount').mockImplementation(async (_, pass: string) => pass === 'pass');
|
||||
jest.spyOn(messaging, 'validateAccount').mockImplementation(async (_, pass: string) => pass === parentPassword);
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
jest.spyOn(messaging, 'validateDerivationPath').mockImplementation(async (_, path) => {
|
||||
if (path === '//') {
|
||||
throw new Error('wrong suri');
|
||||
}
|
||||
|
||||
return {} as ResponseDeriveValidate;
|
||||
return { address: derivedAddress, suri: defaultDerivation } as ResponseDeriveValidate;
|
||||
});
|
||||
|
||||
it('Button is disabled and password field visible', () => {
|
||||
@ -137,7 +146,7 @@ describe('Derive', () => {
|
||||
});
|
||||
|
||||
it('Button is enabled when password is set', async () => {
|
||||
await type(wrapper.find('input[type="password"]'), 'pass');
|
||||
await type(wrapper.find('input[type="password"]'), parentPassword);
|
||||
|
||||
const button = wrapper.find('[data-button-action="create derived account"] button');
|
||||
|
||||
@ -146,7 +155,7 @@ describe('Derive', () => {
|
||||
});
|
||||
|
||||
it('An error is visible and the button is disabled when suri is incorrect', async () => {
|
||||
await type(wrapper.find('input[type="password"]'), 'pass');
|
||||
await type(wrapper.find('input[type="password"]'), parentPassword);
|
||||
await type(wrapper.find('[data-input-suri] input'), '//');
|
||||
wrapper.find('[data-button-action="create derived account"] button').simulate('click');
|
||||
await act(flushAllPromises);
|
||||
@ -160,7 +169,7 @@ describe('Derive', () => {
|
||||
});
|
||||
|
||||
it('The error disappears and "Create derived account" is enabled when typing a new suri', async () => {
|
||||
await type(wrapper.find('input[type="password"]'), 'pass');
|
||||
await type(wrapper.find('input[type="password"]'), parentPassword);
|
||||
await type(wrapper.find('[data-input-suri] input'), '//');
|
||||
wrapper.find('[data-button-action="create derived account"] button').simulate('click');
|
||||
await act(flushAllPromises);
|
||||
@ -192,7 +201,10 @@ describe('Derive', () => {
|
||||
|
||||
describe('Locked parent selection', () => {
|
||||
beforeAll(async () => {
|
||||
wrapper = (await mountComponent(true)).wrapper;
|
||||
const mountedComponent = (await mountComponent(true));
|
||||
|
||||
wrapper = mountedComponent.wrapper;
|
||||
onActionStub = mountedComponent.onActionStub;
|
||||
});
|
||||
|
||||
it('address dropdown does not exist', () => {
|
||||
@ -202,5 +214,29 @@ describe('Derive', () => {
|
||||
it('parent is taken from URL', () => {
|
||||
expect(wrapper.find('[data-field="name"]').first().text()).toBe('B');
|
||||
});
|
||||
|
||||
describe('Second phase', () => {
|
||||
it('correctly creates the derived account', async () => {
|
||||
const newAccount = {
|
||||
name: 'newName',
|
||||
password: 'somePassword'
|
||||
};
|
||||
const deriveMock = jest.spyOn(messaging, 'deriveAccount');
|
||||
|
||||
await type(wrapper.find('input[type="password"]'), parentPassword);
|
||||
wrapper.find('[data-button-action="create derived account"] button').simulate('click');
|
||||
await act(flushAllPromises);
|
||||
wrapper.update();
|
||||
await enterName(newAccount.name).then(password(newAccount.password)).then(repeat(newAccount.password));
|
||||
await act(flushAllPromises);
|
||||
wrapper.update();
|
||||
wrapper.find('[data-button-action="add new root"] button').simulate('click');
|
||||
await act(flushAllPromises);
|
||||
wrapper.update();
|
||||
|
||||
expect(deriveMock).toBeCalledWith(accounts[1].address, defaultDerivation, parentPassword, newAccount.name, newAccount.password, westendGenesis);
|
||||
expect(onActionStub).toBeCalledWith('/');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,10 +1,7 @@
|
||||
// Copyright 2019-2021 @polkadot/extension-ui authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ThemeProps } from '../../types';
|
||||
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { canDerive } from '@polkadot/extension-base/utils';
|
||||
import { assert } from '@polkadot/util';
|
||||
@ -16,7 +13,7 @@ import { nextDerivationPath } from '../../util/nextDerivationPath';
|
||||
import AddressDropdown from './AddressDropdown';
|
||||
import DerivationPath from './DerivationPath';
|
||||
|
||||
interface Props extends ThemeProps {
|
||||
interface Props {
|
||||
className?: string;
|
||||
isLocked?: boolean;
|
||||
parentAddress: string;
|
||||
@ -24,7 +21,7 @@ interface Props extends ThemeProps {
|
||||
onDerivationConfirmed: (derivation: { account: { address: string; suri: string }; parentPassword: string }) => void;
|
||||
}
|
||||
|
||||
function SelectParent ({ className, isLocked, onDerivationConfirmed, parentAddress, parentGenesis }: Props): React.ReactElement<Props> {
|
||||
export default function SelectParent ({ className, isLocked, onDerivationConfirmed, parentAddress, parentGenesis }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const onAction = useContext(ActionContext);
|
||||
const [isBusy, setIsBusy] = useState(false);
|
||||
@ -165,6 +162,3 @@ function SelectParent ({ className, isLocked, onDerivationConfirmed, parentAddre
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default styled(SelectParent)`
|
||||
`;
|
||||
|
||||
@ -4,10 +4,10 @@
|
||||
import React, { useCallback, useContext, useMemo, useState } from 'react';
|
||||
import { useParams } from 'react-router';
|
||||
|
||||
import { AccountContext, ActionContext, Address, BackButton, ButtonArea, NextStepButton, VerticalSpace } from '../../components';
|
||||
import { AccountContext, AccountNamePasswordCreation, ActionContext, Address } from '../../components';
|
||||
import useTranslation from '../../hooks/useTranslation';
|
||||
import { deriveAccount } from '../../messaging';
|
||||
import { HeaderWithSteps, Name, Password } from '../../partials';
|
||||
import { HeaderWithSteps } from '../../partials';
|
||||
import SelectParent from './SelectParent';
|
||||
|
||||
interface Props {
|
||||
@ -35,7 +35,6 @@ function Derive ({ isLocked }: Props): React.ReactElement<Props> {
|
||||
const [isBusy, setIsBusy] = useState(false);
|
||||
const [account, setAccount] = useState<null | PathState>(null);
|
||||
const [name, setName] = useState<string | null>(null);
|
||||
const [password, setPassword] = useState<string | null>(null);
|
||||
const [parentPassword, setParentPassword] = useState<string | null>(null);
|
||||
|
||||
const parentGenesis = useMemo(
|
||||
@ -43,7 +42,7 @@ function Derive ({ isLocked }: Props): React.ReactElement<Props> {
|
||||
[accounts, parentAddress]
|
||||
);
|
||||
|
||||
const _onCreate = useCallback(() => {
|
||||
const _onCreate = useCallback((name: string, password: string) => {
|
||||
if (!account || !name || !password || !parentPassword) {
|
||||
return;
|
||||
}
|
||||
@ -55,7 +54,7 @@ function Derive ({ isLocked }: Props): React.ReactElement<Props> {
|
||||
setIsBusy(false);
|
||||
console.error(error);
|
||||
});
|
||||
}, [account, name, password, onAction, parentAddress, parentGenesis, parentPassword]);
|
||||
}, [account, onAction, parentAddress, parentGenesis, parentPassword]);
|
||||
|
||||
const _onDerivationConfirmed = useCallback(({ account, parentPassword }: ConfirmState) => {
|
||||
setAccount(account);
|
||||
@ -89,22 +88,13 @@ function Derive ({ isLocked }: Props): React.ReactElement<Props> {
|
||||
name={name}
|
||||
/>
|
||||
</div>
|
||||
<Name
|
||||
isFocused
|
||||
onChange={setName}
|
||||
<AccountNamePasswordCreation
|
||||
buttonLabel={t<string>('Create derived account')}
|
||||
isBusy={isBusy}
|
||||
onBackClick={_onBackClick}
|
||||
onCreate={_onCreate}
|
||||
onNameChange={setName}
|
||||
/>
|
||||
<Password onChange={setPassword} />
|
||||
<VerticalSpace/>
|
||||
<ButtonArea>
|
||||
<BackButton onClick={_onBackClick}/>
|
||||
<NextStepButton
|
||||
isBusy={isBusy}
|
||||
isDisabled={!password}
|
||||
onClick={_onCreate}
|
||||
>
|
||||
{t<string>('Create derived account')}
|
||||
</NextStepButton>
|
||||
</ButtonArea>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@ -1,121 +0,0 @@
|
||||
// Copyright 2019-2021 @polkadot/extension-ui authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { AccountContext, ActionContext, Address, ButtonArea, NextStepButton, TextAreaWithLabel, ValidatedInput, VerticalSpace } from '../components';
|
||||
import useTranslation from '../hooks/useTranslation';
|
||||
import { createAccountSuri, validateSeed } from '../messaging';
|
||||
import { Header, Name, Password } from '../partials';
|
||||
import { allOf, isNotShorterThan, Result } from '../util/validators';
|
||||
|
||||
async function validate (suri: string): Promise<Result<string>> {
|
||||
try {
|
||||
await validateSeed(suri);
|
||||
|
||||
return Result.ok(suri);
|
||||
} catch (error) {
|
||||
return Result.error((error as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
const isSeedValid = allOf(
|
||||
isNotShorterThan(1, 'Seed is empty'),
|
||||
validate
|
||||
);
|
||||
|
||||
export default function Import (): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
const { accounts } = useContext(AccountContext);
|
||||
const onAction = useContext(ActionContext);
|
||||
const [isBusy, setIsBusy] = useState(false);
|
||||
const [account, setAccount] = useState<null | { address: string; suri: string }>(null);
|
||||
const [name, setName] = useState<string | null>(null);
|
||||
const [password, setPassword] = useState<string | null>(null);
|
||||
|
||||
useEffect((): void => {
|
||||
!accounts.length && onAction();
|
||||
}, [accounts, onAction]);
|
||||
|
||||
const _onChangeSeed = useCallback(
|
||||
async (suri: string | null): Promise<void> => {
|
||||
if (suri) {
|
||||
try {
|
||||
return setAccount(await validateSeed(suri));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
setAccount(null);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
// FIXME Duplicated between here and Create.tsx
|
||||
const _onCreate = useCallback((): void => {
|
||||
// this should always be the case
|
||||
if (name && password && account) {
|
||||
setIsBusy(true);
|
||||
|
||||
createAccountSuri(name, password, account.suri)
|
||||
.then(() => onAction('/'))
|
||||
.catch((error): void => {
|
||||
setIsBusy(false);
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
}, [account, name, onAction, password]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header
|
||||
showBackArrow
|
||||
smallMargin
|
||||
text={t<string>('Import account')}
|
||||
/>
|
||||
<div>
|
||||
<Address
|
||||
address={account?.address}
|
||||
name={name}
|
||||
/>
|
||||
</div>
|
||||
<ValidatedInput
|
||||
// eslint-disable-next-line @typescript-eslint/no-use-before-define
|
||||
component={SeedInput}
|
||||
isFocused
|
||||
label={t<string>('existing 12 or 24-word mnemonic seed')}
|
||||
onValidatedChange={_onChangeSeed}
|
||||
rowsCount={2}
|
||||
validator={isSeedValid}
|
||||
/>
|
||||
{account && (
|
||||
<>
|
||||
<Name onChange={setName} />
|
||||
<Password onChange={setPassword} />
|
||||
</>
|
||||
)}
|
||||
{account && name && password && (
|
||||
<>
|
||||
<VerticalSpace />
|
||||
<ButtonArea>
|
||||
<NextStepButton
|
||||
isBusy={isBusy}
|
||||
onClick={_onCreate}
|
||||
>
|
||||
{t<string>('Add the account with the supplied seed')}
|
||||
</NextStepButton>
|
||||
</ButtonArea>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const SeedInput = styled(TextAreaWithLabel)`
|
||||
margin-bottom: 16px;
|
||||
textarea {
|
||||
height: unset;
|
||||
}
|
||||
`;
|
||||
148
packages/extension-ui/src/Popup/ImportSeed/SeedAndPath.tsx
Normal file
148
packages/extension-ui/src/Popup/ImportSeed/SeedAndPath.tsx
Normal file
@ -0,0 +1,148 @@
|
||||
// Copyright 2019-2021 @polkadot/extension-ui authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ThemeProps } from '../../types';
|
||||
|
||||
import { faCaretDown, faCaretRight } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { validateSeed } from '@polkadot/extension-ui/messaging';
|
||||
|
||||
import { ButtonArea, InputWithLabel, NextStepButton, TextAreaWithLabel, VerticalSpace, Warning } from '../../components';
|
||||
import useTranslation from '../../hooks/useTranslation';
|
||||
|
||||
interface AccountInfo {
|
||||
address: string;
|
||||
suri: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
className? : string;
|
||||
onNextStep: () => void;
|
||||
onAccountChange: (account: AccountInfo | null) => void;
|
||||
}
|
||||
|
||||
function ImportSeed ({ className, onAccountChange, onNextStep }: Props): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
const [address, setAddress] = useState('');
|
||||
const [seed, setSeed] = useState<string | null>(null);
|
||||
const [path, setPath] = useState<string | null>(null);
|
||||
const [advanced, setAdvances] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
// No need to validate an empty seed
|
||||
// we have a dedicated error for this
|
||||
if (!seed) {
|
||||
onAccountChange(null);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const suri = `${seed || ''}${path || ''}`;
|
||||
|
||||
validateSeed(suri).then((newAccount) => {
|
||||
setError('');
|
||||
setAddress(newAccount.address);
|
||||
onAccountChange(newAccount);
|
||||
}).catch(() => {
|
||||
setAddress('');
|
||||
onAccountChange(null);
|
||||
setError(path
|
||||
? t<string>('Invalid mnemonic seed or derivation path')
|
||||
: t<string>('Invalid mnemonic seed')
|
||||
);
|
||||
});
|
||||
}, [t, seed, path, onAccountChange]);
|
||||
|
||||
const _onToggleAdvanced = useCallback(() => {
|
||||
setAdvances(!advanced);
|
||||
}, [advanced]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={className}>
|
||||
<TextAreaWithLabel
|
||||
className='seedInput'
|
||||
isError={!!error}
|
||||
isFocused
|
||||
label={t<string>('existing 12 or 24-word mnemonic seed')}
|
||||
onChange={setSeed}
|
||||
rowsCount={2}
|
||||
value={seed || ''}
|
||||
/>
|
||||
{!!error && !seed && (
|
||||
<Warning
|
||||
className='seedError'
|
||||
isBelowInput
|
||||
isDanger
|
||||
>
|
||||
{t<string>('Mnemonic needs to contain 12, 15, 18, 21, 24 words')}
|
||||
</Warning>
|
||||
)}
|
||||
<div
|
||||
className='advancedToggle'
|
||||
onClick={_onToggleAdvanced}
|
||||
>
|
||||
<FontAwesomeIcon icon={advanced ? faCaretDown : faCaretRight}/>
|
||||
<span>{t<string>('advanced')}</span>
|
||||
</div>
|
||||
{ advanced && (
|
||||
<InputWithLabel
|
||||
className='derivationPath'
|
||||
isError={!!path && !!error}
|
||||
label={t<string>('derivation path')}
|
||||
onChange={setPath}
|
||||
value={path || ''}
|
||||
/>
|
||||
)}
|
||||
{!!error && !!seed && (
|
||||
<Warning
|
||||
isDanger
|
||||
>
|
||||
{error}
|
||||
</Warning>
|
||||
)}
|
||||
</div>
|
||||
<VerticalSpace />
|
||||
<ButtonArea>
|
||||
<NextStepButton
|
||||
isDisabled={!address || !!error}
|
||||
onClick={onNextStep}
|
||||
>
|
||||
{t<string>('Next')}
|
||||
</NextStepButton>
|
||||
</ButtonArea>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default styled(ImportSeed)(({ theme }: ThemeProps) => `
|
||||
.advancedToggle {
|
||||
color: ${theme.textColor};
|
||||
cursor: pointer;
|
||||
line-height: 14px;
|
||||
letter-spacing: 0.04em;
|
||||
opacity: 0.65;
|
||||
text-transform: uppercase;
|
||||
|
||||
> span {
|
||||
font-size: 10px;
|
||||
margin-left: .5rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.seedInput {
|
||||
margin-bottom: 16px;
|
||||
textarea {
|
||||
height: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.seedError {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
`);
|
||||
73
packages/extension-ui/src/Popup/ImportSeed/index.tsx
Normal file
73
packages/extension-ui/src/Popup/ImportSeed/index.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
// Copyright 2019-2021 @polkadot/extension-ui authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
||||
|
||||
import { AccountContext, ActionContext, Address } from '../../components';
|
||||
import AccountNamePasswordCreation from '../../components/AccountNamePasswordCreation';
|
||||
import useTranslation from '../../hooks/useTranslation';
|
||||
import { createAccountSuri } from '../../messaging';
|
||||
import { HeaderWithSteps } from '../../partials';
|
||||
import SeedAndPath from './SeedAndPath';
|
||||
|
||||
function ImportSeed (): React.ReactElement {
|
||||
const { t } = useTranslation();
|
||||
const { accounts } = useContext(AccountContext);
|
||||
const onAction = useContext(ActionContext);
|
||||
const [isBusy, setIsBusy] = useState(false);
|
||||
const [account, setAccount] = useState<null | { address: string; suri: string }>(null);
|
||||
const [name, setName] = useState<string | null>(null);
|
||||
const [step1, setStep1] = useState(true);
|
||||
|
||||
useEffect((): void => {
|
||||
!accounts.length && onAction();
|
||||
}, [accounts, onAction]);
|
||||
|
||||
const _onCreate = useCallback((name: string, password: string): void => {
|
||||
// this should always be the case
|
||||
if (name && password && account) {
|
||||
setIsBusy(true);
|
||||
|
||||
createAccountSuri(name, password, account.suri)
|
||||
.then(() => onAction('/'))
|
||||
.catch((error): void => {
|
||||
setIsBusy(false);
|
||||
console.error(error);
|
||||
});
|
||||
}
|
||||
}, [account, onAction]);
|
||||
|
||||
const _onNextStep = useCallback(() => { setStep1(false); }, []);
|
||||
const _onBackClick = useCallback(() => { setStep1(true); }, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeaderWithSteps
|
||||
step={step1 ? 1 : 2}
|
||||
text={t<string>('Import account')}
|
||||
/>
|
||||
<div>
|
||||
<Address
|
||||
address={account?.address}
|
||||
name={name}
|
||||
/>
|
||||
</div>
|
||||
{step1
|
||||
? <SeedAndPath
|
||||
onAccountChange={setAccount}
|
||||
onNextStep={_onNextStep}
|
||||
/>
|
||||
: <AccountNamePasswordCreation
|
||||
buttonLabel={t<string>('Add the account with the supplied seed')}
|
||||
isBusy={isBusy}
|
||||
onBackClick={_onBackClick}
|
||||
onCreate={_onCreate}
|
||||
onNameChange={setName}
|
||||
/>
|
||||
|
||||
}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default ImportSeed;
|
||||
@ -0,0 +1,198 @@
|
||||
// Copyright 2019-2021 @polkadot/extension-ui authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import '../../../../__mocks__/chrome';
|
||||
|
||||
import type { ReactWrapper } from 'enzyme';
|
||||
|
||||
import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
|
||||
import { configure, mount } from 'enzyme';
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
import { flushAllPromises } from '../testHelpers';
|
||||
import BackButton from './BackButton';
|
||||
import { AccountNamePasswordCreation, Input, InputWithLabel, NextStepButton } from '.';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-call
|
||||
configure({ adapter: new Adapter() });
|
||||
|
||||
const account = {
|
||||
name: 'My Polkadot Account',
|
||||
password: 'somepassword'
|
||||
};
|
||||
|
||||
const buttonLabel = 'create account';
|
||||
|
||||
let wrapper: ReactWrapper;
|
||||
const onBackClick = jest.fn();
|
||||
const onCreate = jest.fn();
|
||||
const onNameChange = jest.fn();
|
||||
|
||||
const type = async (input: ReactWrapper, value: string): Promise<void> => {
|
||||
input.simulate('change', { target: { value } });
|
||||
await act(flushAllPromises);
|
||||
wrapper.update();
|
||||
};
|
||||
|
||||
const capsLockOn = async (input: ReactWrapper): Promise<void> => {
|
||||
input.simulate('keyPress', { getModifierState: () => true });
|
||||
await act(flushAllPromises);
|
||||
wrapper.update();
|
||||
};
|
||||
|
||||
const enterName = (name: string): Promise<void> => type(wrapper.find('input').first(), name);
|
||||
const password = (password: string) => (): Promise<void> => type(wrapper.find('input[type="password"]').first(), password);
|
||||
const repeat = (password: string) => (): Promise<void> => type(wrapper.find('input[type="password"]').last(), password);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
const mountComponent = (isBusy = false): ReactWrapper => mount(
|
||||
<AccountNamePasswordCreation
|
||||
buttonLabel={buttonLabel}
|
||||
isBusy={isBusy}
|
||||
onBackClick={onBackClick}
|
||||
onCreate={onCreate}
|
||||
onNameChange={onNameChange}
|
||||
/>
|
||||
);
|
||||
|
||||
describe('AccountNamePasswordCreation', () => {
|
||||
beforeEach(async () => {
|
||||
wrapper = mountComponent();
|
||||
await act(flushAllPromises);
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
it('only account name input is visible at first', () => {
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-name]').find(Input)).toHaveLength(1);
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-password]').find(Input)).toHaveLength(1);
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-repeat-password]')).toHaveLength(0);
|
||||
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('next step button has the correct label', () => {
|
||||
expect(wrapper.find(NextStepButton).text()).toBe(buttonLabel);
|
||||
});
|
||||
|
||||
it('back button calls onBackClick', () => {
|
||||
wrapper.find(BackButton).simulate('click');
|
||||
expect(onBackClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('input should not be highlighted as error until first interaction', () => {
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-name]').find(Input).prop('withError')).toBe(false);
|
||||
});
|
||||
|
||||
it('after typing less than 3 characters into name input, password input is not visible', async () => {
|
||||
await enterName('ab');
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-name]').find(Input).prop('withError')).toBe(true);
|
||||
expect(wrapper.find('.warning-message').first().text()).toBe('Account name is too short');
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-password]').find(Input)).toHaveLength(1);
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-repeat-password]')).toHaveLength(0);
|
||||
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('input should keep showing error when something has been typed but then erased', async () => {
|
||||
await enterName(account.name);
|
||||
await enterName('');
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-name]').find(Input).prop('withError')).toBe(true);
|
||||
});
|
||||
|
||||
it('after typing 3 characters into name input, onNameChange is called', async () => {
|
||||
await enterName(account.name);
|
||||
expect(onNameChange).toHaveBeenLastCalledWith(account.name);
|
||||
});
|
||||
|
||||
it('after typing 3 characters into name input, first password input is visible', async () => {
|
||||
await enterName(account.name);
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-name]').find(Input).first().prop('withError')).toBe(false);
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-password]').find(Input)).toHaveLength(1);
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-repeat-password]')).toHaveLength(0);
|
||||
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('password with caps lock should show a warning', async () => {
|
||||
await enterName('abc').then(password('abcde'));
|
||||
await capsLockOn(wrapper.find(InputWithLabel).find('[data-input-password]').find(Input));
|
||||
|
||||
expect(wrapper.find('.warning-message').first().text()).toBe('Warning: Caps lock is on');
|
||||
});
|
||||
|
||||
it('password shorter than 6 characters should be not valid', async () => {
|
||||
await enterName('abc').then(password('abcde'));
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-password]').find(Input).prop('withError')).toBe(true);
|
||||
expect(wrapper.find('.warning-message').text()).toBe('Password is too short');
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-password]').find(Input)).toHaveLength(1);
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-repeat-password]')).toHaveLength(0);
|
||||
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('submit button is not enabled until both passwords are equal', async () => {
|
||||
await enterName('abc').then(password('abcdef')).then(repeat('abcdeg'));
|
||||
expect(wrapper.find('.warning-message').text()).toBe('Passwords do not match');
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-repeat-password]').find(Input).prop('withError')).toBe(true);
|
||||
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('submit button is enabled when both passwords are equal', async () => {
|
||||
await enterName('abc').then(password('abcdef')).then(repeat('abcdef'));
|
||||
expect(wrapper.find('.warning-message')).toHaveLength(0);
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-repeat-password]').find(Input).prop('withError')).toBe(false);
|
||||
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(false);
|
||||
});
|
||||
|
||||
it('calls onCreate with provided name and password', async () => {
|
||||
await enterName(account.name).then(password(account.password)).then(repeat(account.password));
|
||||
wrapper.find('[data-button-action="add new root"] button').simulate('click');
|
||||
await act(flushAllPromises);
|
||||
|
||||
expect(onCreate).toBeCalledWith(account.name, account.password);
|
||||
});
|
||||
|
||||
describe('All fields are filled correctly, but then', () => {
|
||||
beforeEach(async () => {
|
||||
await enterName(account.name).then(password(account.password)).then(repeat(account.password));
|
||||
});
|
||||
|
||||
it('first password input is cleared - second one disappears and button get disabled', async () => {
|
||||
await type(wrapper.find('input[type="password"]').first(), '');
|
||||
expect(wrapper.find(InputWithLabel).find('[data-input-repeat-password]')).toHaveLength(0);
|
||||
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('first password changes - button is disabled', async () => {
|
||||
await type(wrapper.find('input[type="password"]').first(), 'abcdef');
|
||||
expect(wrapper.find('.warning-message').text()).toBe('Passwords do not match');
|
||||
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('first password changes, then second changes too - button is enabled', async () => {
|
||||
await type(wrapper.find('input[type="password"]').first(), 'abcdef');
|
||||
await type(wrapper.find('input[type="password"]').last(), 'abcdef');
|
||||
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(false);
|
||||
});
|
||||
|
||||
it('second password changes, then first changes too - button is enabled', async () => {
|
||||
await type(wrapper.find('input[type="password"]').last(), 'abcdef');
|
||||
await type(wrapper.find('input[type="password"]').first(), 'abcdef');
|
||||
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(false);
|
||||
});
|
||||
|
||||
it('name is removed - button is disabled', async () => {
|
||||
await enterName('');
|
||||
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('AccountNamePasswordCreation busy button', () => {
|
||||
beforeAll(async () => {
|
||||
wrapper = mountComponent(true);
|
||||
await act(flushAllPromises);
|
||||
wrapper.update();
|
||||
});
|
||||
|
||||
it('button is busy', () => {
|
||||
expect(wrapper.find(NextStepButton).prop('isBusy')).toBe(true);
|
||||
});
|
||||
});
|
||||
@ -3,20 +3,18 @@
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
|
||||
import { BackButton, ButtonArea, NextStepButton, VerticalSpace } from '../../components';
|
||||
import useTranslation from '../../hooks/useTranslation';
|
||||
import { Name, Password } from '../../partials';
|
||||
import { Name, Password } from '../partials';
|
||||
import { BackButton, ButtonArea, NextStepButton, VerticalSpace } from '.';
|
||||
|
||||
interface Props {
|
||||
address: string;
|
||||
buttonLabel: string;
|
||||
isBusy: boolean;
|
||||
onBackClick: () => void;
|
||||
onCreate: (name: string, password: string) => void | Promise<void | boolean>;
|
||||
onNameChange: (name: string) => void;
|
||||
}
|
||||
|
||||
function AccountName ({ isBusy, onBackClick, onCreate, onNameChange }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
function AccountNamePasswordCreation ({ buttonLabel, isBusy, onBackClick, onCreate, onNameChange }: Props): React.ReactElement<Props> {
|
||||
const [name, setName] = useState<string | null>(null);
|
||||
const [password, setPassword] = useState<string | null>(null);
|
||||
|
||||
@ -25,10 +23,22 @@ function AccountName ({ isBusy, onBackClick, onCreate, onNameChange }: Props): R
|
||||
[name, password, onCreate]
|
||||
);
|
||||
|
||||
const _onNameChange = useCallback((name: string | null) => {
|
||||
onNameChange(name || '');
|
||||
setName(name);
|
||||
}, [onNameChange]);
|
||||
const _onNameChange = useCallback(
|
||||
(name: string | null) => {
|
||||
onNameChange(name || '');
|
||||
setName(name);
|
||||
},
|
||||
[onNameChange]
|
||||
);
|
||||
|
||||
const _onBackClick = useCallback(
|
||||
() => {
|
||||
_onNameChange(null);
|
||||
setPassword(null);
|
||||
onBackClick();
|
||||
},
|
||||
[_onNameChange, onBackClick]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -39,18 +49,18 @@ function AccountName ({ isBusy, onBackClick, onCreate, onNameChange }: Props): R
|
||||
<Password onChange={setPassword} />
|
||||
<VerticalSpace />
|
||||
<ButtonArea>
|
||||
<BackButton onClick={onBackClick} />
|
||||
<BackButton onClick={_onBackClick} />
|
||||
<NextStepButton
|
||||
data-button-action='add new root'
|
||||
isBusy={isBusy}
|
||||
isDisabled={!password || !name}
|
||||
onClick={_onCreate}
|
||||
>
|
||||
{t<string>('Add the account with the generated seed')}
|
||||
{buttonLabel}
|
||||
</NextStepButton>
|
||||
</ButtonArea>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default React.memo(AccountName);
|
||||
export default React.memo(AccountNamePasswordCreation);
|
||||
@ -107,6 +107,8 @@ function Address ({ actions, address, children, className, genesisHash, isExtern
|
||||
|
||||
useEffect((): void => {
|
||||
if (!address) {
|
||||
setRecoded(defaultRecoded);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
// Copyright 2019-2021 @polkadot/extension-ui authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
export { default as AccountNamePasswordCreation } from './AccountNamePasswordCreation';
|
||||
export { default as ActionBar } from './ActionBar';
|
||||
export { default as ActionText } from './ActionText';
|
||||
export { default as Address } from './Address';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user