Skip to content

Commit

Permalink
Create new account as a child of a master account by default (#318)
Browse files Browse the repository at this point in the history
* derive accounts for master account

* Add master field to account context (#75)

* add master field to account context

* add tests for buildHierarchy function

* Fix selection master account

* Add path numeration for derived accounts (#76)

* add path numeration for derived accounts

* Fix PR notices. Add util for Path generation.

* Correct test

Co-authored-by: Ivan Rukhavets <ivanruch@gmail.com>
Co-authored-by: VladStarostenko <vlad@DESKTOP-V12SRE8.localdomain>

* Add arrows to buttons (#77)

* Add parent label (#80)

* refactor parent label

* refactor parent label

* refactor parent label

Co-authored-by: Ivan Rukhavets <ivanruch@gmail.com>

* Implement new create account flow (#78)

* Add copied snackbar (#83)

* refactor parent label

* Add "Copied" snackbar

* Add "Copied" snackbar

* Hide suri input behind a spoiler (#84)

* Disallow changing parent account when 'Derive' option is clicked (#82)

* Add lock to path input (#85)

Co-authored-by: VladStarostenko <38181661+VladStarostenko@users.noreply.github.com>
Co-authored-by: VladStarostenko <vlad@DESKTOP-V12SRE8.localdomain>
  • Loading branch information
3 people committed May 11, 2020
1 parent 05d3f78 commit ed36ed2
Show file tree
Hide file tree
Showing 35 changed files with 873 additions and 216 deletions.
2 changes: 2 additions & 0 deletions packages/extension-base/src/background/types.ts
Expand Up @@ -29,6 +29,7 @@ export interface AccountJson extends KeyringPair$Meta {
isExternal?: boolean;
name?: string;
parentAddress?: string;
whenCreated?: number;
}

export type AccountWithChildren = AccountJson & {
Expand All @@ -38,6 +39,7 @@ export type AccountWithChildren = AccountJson & {
export type AccountsContext = {
accounts: AccountJson[];
hierarchy: AccountWithChildren[];
master?: AccountJson;
}

export interface AuthorizeRequest {
Expand Down
2 changes: 1 addition & 1 deletion packages/extension-ui/src/Popup/Accounts/Account.tsx
Expand Up @@ -41,7 +41,7 @@ function Account ({ address, className, isExternal, parentName }: Props): React.
<MenuGroup>
<MenuItem onClick={_toggleEdit}>Rename</MenuItem>
{!isExternal && (
<MenuItem to={`/account/derive/${address}`}>Derive New Account</MenuItem>
<MenuItem to={`/account/derive/${address}/locked`}>Derive New Account</MenuItem>
)}
</MenuGroup>
{!isExternal && (
Expand Down
10 changes: 5 additions & 5 deletions packages/extension-ui/src/Popup/Accounts/index.tsx
Expand Up @@ -5,15 +5,15 @@
import React, { useContext } from 'react';
import styled from 'styled-components';

import QrImage from '../../assets/qr.svg';
import { AccountContext, Button, ButtonArea, ButtonWithSubtitle, MediaContext, Svg } from '../../components';
import { AddAccount, Header } from '../../partials';
import AccountsTree from './AccountsTree';
import qrIcon from '../../assets/qr.svg';

type Props = {};

export default function Accounts (): React.ReactElement<Props> {
const { hierarchy } = useContext(AccountContext);
const { hierarchy, master } = useContext(AccountContext);
const mediaAllowed = useContext(MediaContext);

return (
Expand All @@ -39,9 +39,9 @@ export default function Accounts (): React.ReactElement<Props> {
}
<ButtonArea>
<ButtonWithSubtitle
subTitle='With new seed'
subTitle={master ? 'Derive from master' : 'With new seed'}
title='Create New Account'
to='/account/create'
to={master ? `/account/derive/${master.address}` : '/account/create'}
/>
<ButtonWithSubtitle
subTitle='I have a pre-existing seed'
Expand All @@ -50,7 +50,7 @@ export default function Accounts (): React.ReactElement<Props> {
/>
{mediaAllowed && (
<QrButton to='/account/import-qr'>
<Svg src={QrImage} />
<Svg src={qrIcon} />
</QrButton>
)}
</ButtonArea>
Expand Down
42 changes: 23 additions & 19 deletions packages/extension-ui/src/Popup/CreateAccount/AccountName.tsx
Expand Up @@ -2,19 +2,22 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.

import React, { useState } from 'react';
import { Address, Button, ButtonArea, VerticalSpace } from '@polkadot/extension-ui/components';
import { Name, Password } from '@polkadot/extension-ui/partials';
import React, { useCallback, useState } from 'react';
import { Address, BackButton, ButtonArea, NextStepButton, VerticalSpace } from '../..//components';
import { Name, Password } from '../../partials';

interface Props {
onCreate: (name: string, password: string) => void | Promise<void | boolean>;
address: string;
onBackClick: () => void;
onCreate: (name: string, password: string) => void | Promise<void | boolean>;
}

function AccountName ({ address, onCreate }: Props): React.ReactElement<Props> {
function AccountName ({ address, onBackClick, onCreate }: Props): React.ReactElement<Props> {
const [name, setName] = useState<string | null>(null);
const [password, setPassword] = useState<string | null>(null);

const _onCreate = useCallback(() => name && password && onCreate(name, password), [name, password, onCreate]);

return (
<>
<Name
Expand All @@ -23,21 +26,22 @@ function AccountName ({ address, onCreate }: Props): React.ReactElement<Props> {
/>
{name && <Password onChange={setPassword} />}
{name && password && (
<>
<Address
address={address}
name={name}
/>
<VerticalSpace />
<ButtonArea>
<Button
onClick={(): void | Promise<void | boolean> => onCreate(name, password)}
>
Add the account with the generated seed
</Button>
</ButtonArea>
</>
<Address
address={address}
name={name}
/>
)}
<VerticalSpace />
<ButtonArea>
<BackButton onClick={onBackClick} />
<NextStepButton
data-button-action='add new root'
isDisabled={!password || !name}
onClick={_onCreate}
>
Add the account with the generated seed
</NextStepButton>
</ButtonArea>
</>
);
}
Expand Down
Expand Up @@ -75,7 +75,7 @@ describe('Create Account', () => {
it('clicking on Next activates phase 2', () => {
check(wrapper.find('input[type="checkbox"]'));
wrapper.find('button').simulate('click');
expect(wrapper.find(Header).text()).toBe('Create an account 2/2Back');
expect(wrapper.find(Header).text()).toBe('Create an account: 2/2Cancel');
});
});

Expand All @@ -90,7 +90,7 @@ describe('Create Account', () => {
expect(wrapper.find(InputWithLabel).find('[data-input-name]').find(Input)).toHaveLength(1);
expect(wrapper.find(InputWithLabel).find('[data-input-password]')).toHaveLength(0);
expect(wrapper.find(InputWithLabel).find('[data-input-repeat-password]')).toHaveLength(0);
expect(wrapper.find(Button)).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', () => {
Expand All @@ -103,7 +103,7 @@ describe('Create Account', () => {
expect(wrapper.find('ErrorMessage').text()).toBe('Account name is too short');
expect(wrapper.find(InputWithLabel).find('[data-input-password]')).toHaveLength(0);
expect(wrapper.find(InputWithLabel).find('[data-input-repeat-password]')).toHaveLength(0);
expect(wrapper.find(Button)).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 () => {
Expand All @@ -117,7 +117,7 @@ describe('Create Account', () => {
expect(wrapper.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(Button)).toHaveLength(0);
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(true);
});

it('password shorter than 6 characters should be not valid', async () => {
Expand All @@ -126,21 +126,21 @@ describe('Create Account', () => {
expect(wrapper.find('ErrorMessage').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(Button)).toHaveLength(0);
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(true);
});

it('submit button is not visible until both passwords are equal', async () => {
await enterName('abc').then(password('abcdef')).then(repeat('abcdeg'));
expect(wrapper.find('ErrorMessage').text()).toBe('Passwords do not match');
expect(wrapper.find(InputWithLabel).find('[data-input-repeat-password]').find(Input).prop('withError')).toBe(true);
expect(wrapper.find(Button)).toHaveLength(0);
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(true);
});

it('submit button is visible when both passwords are equal', async () => {
await enterName('abc').then(password('abcdef')).then(repeat('abcdef'));
expect(wrapper.find('ErrorMessage')).toHaveLength(0);
expect(wrapper.find(InputWithLabel).find('[data-input-repeat-password]').find(Input).prop('withError')).toBe(false);
expect(wrapper.find(Button)).toHaveLength(1);
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(false);
});

it('saves account with provided name and password', async () => {
Expand All @@ -161,28 +161,28 @@ describe('Create Account', () => {
await enterName('abc').then(password('abcdef')).then(repeat('abcdef'));
});

it('first password input is cleared - second one and button get hidden', async () => {
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(Button)).toHaveLength(0);
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(true);
});

it('first password changes - button is not visible', async () => {
it('first password changes - button is not disabled', async () => {
await type(wrapper.find('input[type="password"]').first(), 'aaaaaa');
expect(wrapper.find('ErrorMessage').text()).toBe('Passwords do not match');
expect(wrapper.find(Button)).toHaveLength(0);
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(true);
});

it('first password changes, then second changes too - button is visible', async () => {
await type(wrapper.find('input[type="password"]').first(), 'aaaaaa');
await type(wrapper.find('input[type="password"]').last(), 'aaaaaa');
expect(wrapper.find(Button)).toHaveLength(1);
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(false);
});

it('second password changes, then first changes too - button is visible', async () => {
await type(wrapper.find('input[type="password"]').last(), 'aaaaaa');
await type(wrapper.find('input[type="password"]').first(), 'aaaaaa');
expect(wrapper.find(Button)).toHaveLength(1);
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(false);
});
});
});
6 changes: 3 additions & 3 deletions packages/extension-ui/src/Popup/CreateAccount/Mnemonic.tsx
Expand Up @@ -3,7 +3,7 @@
// of the Apache-2.0 license. See the LICENSE file for details.

import React, { useCallback, useState } from 'react';
import { Button, ButtonArea, Checkbox, MnemonicSeed, VerticalSpace, Warning } from '../../components';
import { ButtonArea, Checkbox, MnemonicSeed, NextStepButton, VerticalSpace, Warning } from '../../components';
import useToast from '../../hooks/useToast';

interface Props {
Expand Down Expand Up @@ -45,12 +45,12 @@ function Mnemonic ({ onNextStep, seed }: Props): React.ReactElement<Props> {
onChange={setIsMnemonicSaved}
/>
<ButtonArea>
<Button
<NextStepButton
isDisabled={!isMnemonicSaved}
onClick={onNextStep}
>
Next step
</Button>
</NextStepButton>
</ButtonArea>
</>
);
Expand Down
56 changes: 8 additions & 48 deletions packages/extension-ui/src/Popup/CreateAccount/index.tsx
Expand Up @@ -3,11 +3,9 @@
// of the Apache-2.0 license. See the LICENSE file for details.

import React, { useContext, useEffect, useState } from 'react';
import styled from 'styled-components';

import { ActionContext, Loading, ActionText } from '../../components';
import { ActionContext, Loading } from '../../components';
import { createAccountSuri, createSeed } from '../../messaging';
import { Header } from '../../partials';
import { HeaderWithSteps } from '../../partials';
import AccountName from './AccountName';
import Mnemonic from './Mnemonic';

Expand All @@ -33,29 +31,14 @@ export default function CreateAccount (): React.ReactElement {
};

const _onNextStep = (): void => setStep(step + 1);

const _onCancel = (): void => {
if (step === 2) {
setStep(step - 1);
} else {
onAction('/');
}
};
const _onPreviousStep = (): void => setStep(step - 1);

return (
<>
<Header text={'Create an account '}>
<CreationSteps>
<div>
<CurrentStep>{step}</CurrentStep>
<TotalSteps>/2</TotalSteps>
</div>
<ActionText
onClick={_onCancel}
text={step === 1 ? 'Cancel' : 'Back'}
/>
</CreationSteps>
</Header>
<HeaderWithSteps
step={step}
text='Create an account:&nbsp;'
/>
<Loading>{account && (step === 1
? (
<Mnemonic
Expand All @@ -66,34 +49,11 @@ export default function CreateAccount (): React.ReactElement {
: (
<AccountName
address={account.address}
onBackClick={_onPreviousStep}
onCreate={_onCreate}
/>
)
)}</Loading>
</>
);
}

const CreationSteps = styled.div`
align-items: center;
display: flex;
justify-content: space-between;
flex-grow: 1;
padding-right: 24px;
margin-top: 3px;
`;

const CurrentStep = styled.span`
font-size: ${({ theme }): string => theme.labelFontSize};
line-height: ${({ theme }): string => theme.labelLineHeight};
color: ${({ theme }): string => theme.primaryColor};
font-weight: 800;
margin-left: 10px;
`;

const TotalSteps = styled.span`
font-size: ${({ theme }): string => theme.labelFontSize};
line-height: ${({ theme }): string => theme.labelLineHeight};
color: ${({ theme }): string => theme.textColor};
font-weight: 800;
`;

0 comments on commit ed36ed2

Please sign in to comment.