Repository: polkadot-js/extension (ts)

Issue:
Split account creation and derivation from the + menu
As mentioned in https://github.com/polkadot-js/extension/pull/586#pullrequestreview-551257447
Having an additional 2 clicks to create a new account is a little too much.
We should split those  2 flows with dedicated entries

Pull request description:
Separate derivation and creation flow
closes #588 

![creation-flow](https://user-images.githubusercontent.com/33178835/102767175-73f0ca80-437f-11eb-8893-14863a7498fe.gif)


Patch:
diff --git a/README.md b/README.md
index c7ac441e..a866c8c8 100644
--- a/README.md
+++ b/README.md
@@ -17,25 +17,8 @@ As it stands, it does one thing: it _only_ manages accounts and allows the signi
 
 ![interface screenshots](docs/extension-overview.png)
 
-## FAQ
-
-### I want to send funds directly from the extension.
-The extension is not meant to be a full wallet replacement. It tries to focus on pure account management while making this as smooth as possible. The extension simply makes the stored/imported accounts available to any dApp that can perform transfers, allow you to vote on democracy proposals and/or participate in any feature available on Polkadot and Substrate. We really aim to keep this singular account-only focus with no network connectivity, leaving the hard-work of understanding chains and their intricacies over to over to wallets such as [polkadot-js/apps](https://polkadot.js.org/apps/).
-
-### My addresses display differently on the extension vs a dapp.
-Unlike a dapp, the extension itself does not connect to a network. The addresses are formatted with a network-specific prefix, which on a dapp, is retrieved upon connection from the chain properties. Underlying the public keys can be the same, but based on the address formatting the display could be different. This does not affect operation at all. You can choose which format to display the addresses in your extension via the "Options" screen.
-
-### What does it mean to derive account?
-We can imagine that accounts are stored as a tree. It allows us to structure accounts by our own needs. The root account is created at first and all the new ones will be its children by default. If we want to derive from other accounts, it can be done by selecting Derive New Account option in parent account’s context menu. Before deriving an account, the [HDKD derivation path](https://github.com/paritytech/parity-signer/wiki/HDKD-on-Parity-Signer#the-form-of-path) can be provided - it is a great tool that could be used for further account structuring.
-
-The format of a derivation path is `//<hard>/<soft>///<password>` where
-* `//hard` means that the connection between accounts is not “visible” for public addresses.
-* `/soft` connects accounts in a manner that can be proven.
-* `///password` provides an additional (optional) encryption.
-
-### Why can't I import an account with a hex-encoded private key?
-This is a deliberate choice since mnemonic phrases are easier to write down, to memorize or type in a field. They have a checksum (not every 12/24 words list is a valid mnemonic) and constitute a much better means of holding un-encrypted information. Therefore, this extension does not and will not have the functionality to recover an account from a hex seed phrase.
-
+## Documentation and examples
+Find out more about how to use the extension as a Dapp developper, cookbook, as well as answers to most frequent questions in the [Polkadot-js extension docuementation](https://polkadot.js.org/docs/extension/)
 
 ## Development version
 
diff --git a/package.json b/package.json
index 353cd618..256a9394 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
   "repository": "https://github.com/polkadot-js/extension",
   "author": "Jaco Greeff <jacogr@gmail.com>",
   "license": "Apache-2",
-  "version": "0.35.2-19",
+  "version": "0.35.2-22",
   "private": true,
   "workspaces": [
     "packages/*"
diff --git a/packages/extension-base/package.json b/packages/extension-base/package.json
index 7348ed3b..88ee757d 100644
--- a/packages/extension-base/package.json
+++ b/packages/extension-base/package.json
@@ -1,7 +1,7 @@
 {
   "name": "@polkadot/extension-base",
   "description": "Functions, classes and other utilities used in @polkadot/extension",
-  "version": "0.35.2-19",
+  "version": "0.35.2-22",
   "author": "Jaco Greeff <jacogr@gmail.com>",
   "license": "Apache-2",
   "main": "index.js",
@@ -9,7 +9,7 @@
   "dependencies": {
     "@babel/runtime": "^7.12.5",
     "@polkadot/api": "^3.0.2-8",
-    "@polkadot/extension-inject": "^0.35.2-19",
+    "@polkadot/extension-inject": "^0.35.2-22",
     "@polkadot/keyring": "^5.0.2-7",
     "@polkadot/phishing": "^0.3.1",
     "@polkadot/ui-keyring": "^0.63.2-2"
diff --git a/packages/extension-chains/package.json b/packages/extension-chains/package.json
index 543dc3e8..a4ecfbdd 100644
--- a/packages/extension-chains/package.json
+++ b/packages/extension-chains/package.json
@@ -1,7 +1,7 @@
 {
   "name": "@polkadot/extension-chains",
   "description": "Definitions for all known chains as exposed by the extension.",
-  "version": "0.35.2-19",
+  "version": "0.35.2-22",
   "author": "Jaco Greeff <jacogr@gmail.com>",
   "license": "Apache-2",
   "main": "index.js",
diff --git a/packages/extension-dapp/package.json b/packages/extension-dapp/package.json
index e80f21ae..6b548c5d 100644
--- a/packages/extension-dapp/package.json
+++ b/packages/extension-dapp/package.json
@@ -1,14 +1,14 @@
 {
   "name": "@polkadot/extension-dapp",
   "description": "Provides an interfaces around the injected globals for ease of access by dapp developers.",
-  "version": "0.35.2-19",
+  "version": "0.35.2-22",
   "author": "Jaco Greeff <jacogr@gmail.com>",
   "license": "Apache-2",
   "main": "index.js",
   "sideEffects": false,
   "dependencies": {
     "@babel/runtime": "^7.12.5",
-    "@polkadot/extension-inject": "^0.35.2-19",
+    "@polkadot/extension-inject": "^0.35.2-22",
     "@polkadot/util": "^5.0.2-7",
     "@polkadot/util-crypto": "^5.0.2-7"
   },
diff --git a/packages/extension-inject/package.json b/packages/extension-inject/package.json
index 2d55a0dc..13f6502b 100644
--- a/packages/extension-inject/package.json
+++ b/packages/extension-inject/package.json
@@ -1,7 +1,7 @@
 {
   "name": "@polkadot/extension-inject",
   "description": "A generic injector (usable to any extension), that populates the base exposed interfaces to be used by dapps.",
-  "version": "0.35.2-19",
+  "version": "0.35.2-22",
   "author": "Jaco Greeff <jacogr@gmail.com>",
   "license": "Apache-2",
   "main": "index.js",
diff --git a/packages/extension-ui/package.json b/packages/extension-ui/package.json
index 1a4763ee..f45fecec 100644
--- a/packages/extension-ui/package.json
+++ b/packages/extension-ui/package.json
@@ -1,7 +1,7 @@
 {
   "name": "@polkadot/extension-ui",
   "description": "A sample signer extension for the @polkadot/api",
-  "version": "0.35.2-19",
+  "version": "0.35.2-22",
   "author": "Jaco Greeff <jacogr@gmail.com>",
   "license": "Apache-2",
   "dependencies": {
@@ -10,8 +10,8 @@
     "@fortawesome/free-regular-svg-icons": "^5.15.1",
     "@fortawesome/free-solid-svg-icons": "^5.15.1",
     "@fortawesome/react-fontawesome": "^0.1.13",
-    "@polkadot/extension-base": "^0.35.2-19",
-    "@polkadot/extension-inject": "^0.35.2-19",
+    "@polkadot/extension-base": "^0.35.2-22",
+    "@polkadot/extension-inject": "^0.35.2-22",
     "@polkadot/keyring": "^5.0.2-7",
     "@polkadot/react-identicon": "^0.63.2-2",
     "@polkadot/react-qr": "^0.63.2-2",
diff --git a/packages/extension-ui/src/Popup/Derive/SelectParent.tsx b/packages/extension-ui/src/Popup/Derive/SelectParent.tsx
index 6d282866..edb5ae80 100644
--- a/packages/extension-ui/src/Popup/Derive/SelectParent.tsx
+++ b/packages/extension-ui/src/Popup/Derive/SelectParent.tsx
@@ -9,7 +9,7 @@ import styled from 'styled-components';
 import { canDerive } from '@polkadot/extension-base/utils';
 import { assert } from '@polkadot/util';
 
-import { AccountContext, ActionContext, Address, ButtonArea, Checkbox, InputWithLabel, Label, NextStepButton, VerticalSpace, Warning } from '../../components';
+import { AccountContext, ActionContext, Address, ButtonArea, InputWithLabel, Label, NextStepButton, VerticalSpace, Warning } from '../../components';
 import useTranslation from '../../hooks/useTranslation';
 import { validateAccount, validateDerivationPath } from '../../messaging';
 import { nextDerivationPath } from '../../util/nextDerivationPath';
@@ -33,7 +33,6 @@ function SelectParent ({ className, isLocked, onDerivationConfirmed, parentAddre
   const [suriPath, setSuriPath] = useState<null | string>(defaultPath);
   const [parentPassword, setParentPassword] = useState<string>('');
   const [isProperParentPassword, setIsProperParentPassword] = useState(false);
-  const [shouldAccountBeDerived, setShouldAccountBeDerived] = useState(true);
   const passwordInputRef = useRef<HTMLDivElement>(null);
 
   const allAddresses = useMemo(
@@ -44,11 +43,6 @@ function SelectParent ({ className, isLocked, onDerivationConfirmed, parentAddre
     [hierarchy]
   );
 
-  const _goCreate = useCallback(
-    () => onAction('/account/create'),
-    [onAction]
-  );
-
   const _onParentPasswordEnter = useCallback(
     (parentPassword: string): void => {
       setParentPassword(parentPassword);
@@ -98,15 +92,7 @@ function SelectParent ({ className, isLocked, onDerivationConfirmed, parentAddre
   return (
     <>
       <div className={className}>
-        {!isLocked && (
-          <Checkbox
-            checked={shouldAccountBeDerived}
-            className='smallerMargin'
-            label={t<string>('Derive new account from existing')}
-            onChange={setShouldAccountBeDerived}
-          />
-        )}
-        <div className={`disableArea ${shouldAccountBeDerived ? '' : 'disabled'}`}>
+        <div className=''>
           {isLocked
             ? (
               <Address
@@ -125,45 +111,41 @@ function SelectParent ({ className, isLocked, onDerivationConfirmed, parentAddre
               </Label>
             )
           }
-          {shouldAccountBeDerived && (
+          <div ref={passwordInputRef}>
+            <InputWithLabel
+              data-input-password
+              isError={!!parentPassword && !isProperParentPassword}
+              isFocused
+              label={t<string>('enter the password for the account you want to derive from')}
+              onChange={_onParentPasswordEnter}
+              type='password'
+              value={parentPassword}
+            />
+            {!!parentPassword && !isProperParentPassword && (
+              <Warning
+                isBelowInput
+                isDanger
+              >
+                {t('Wrong password')}
+              </Warning>
+            )}
+          </div>
+          {isProperParentPassword && (
             <>
-              <div ref={passwordInputRef}>
-                <InputWithLabel
-                  data-export-password
-                  isError={!isProperParentPassword}
-                  isFocused
-                  label={t<string>('enter the password for the account you want to derive from')}
-                  onChange={_onParentPasswordEnter}
-                  type='password'
-                  value={parentPassword}
-                />
-                {!!parentPassword && !isProperParentPassword && (
-                  <Warning
-                    isBelowInput
-                    isDanger
-                  >
-                    {t('Wrong password')}
-                  </Warning>
-                )}
-              </div>
-              {isProperParentPassword && (
-                <>
-                  <DerivationPath
-                    defaultPath={defaultPath}
-                    isError={!suriPath}
-                    onChange={setSuriPath}
-                    parentAddress={parentAddress}
-                    parentPassword={parentPassword}
-                  />
-                  {!suriPath && (
-                    <Warning
-                      isBelowInput
-                      isDanger
-                    >
-                      {t('Incorrect derivation path')}
-                    </Warning>
-                  )}
-                </>
+              <DerivationPath
+                defaultPath={defaultPath}
+                isError={!suriPath}
+                onChange={setSuriPath}
+                parentAddress={parentAddress}
+                parentPassword={parentPassword}
+              />
+              {!suriPath && (
+                <Warning
+                  isBelowInput
+                  isDanger
+                >
+                  {t('Incorrect derivation path')}
+                </Warning>
               )}
             </>
           )}
@@ -171,43 +153,18 @@ function SelectParent ({ className, isLocked, onDerivationConfirmed, parentAddre
       </div>
       <VerticalSpace/>
       <ButtonArea>
-        {shouldAccountBeDerived
-          ? (
-            <NextStepButton
-              data-button-action='create derived account'
-              isBusy={isBusy}
-              isDisabled={!isProperParentPassword || !suriPath}
-              onClick={_onSubmit}
-            >
-              {t<string>('Create a derived account')}
-            </NextStepButton>
-          )
-          : (
-            <NextStepButton
-              data-button-action='create root account'
-              onClick={_goCreate}
-            >
-              {t<string>('Create account from new seed')}
-            </NextStepButton>
-          )
-        }
+        <NextStepButton
+          data-button-action='create derived account'
+          isBusy={isBusy}
+          isDisabled={!isProperParentPassword || !suriPath}
+          onClick={_onSubmit}
+        >
+          {t<string>('Create a derived account')}
+        </NextStepButton>
       </ButtonArea>
     </>
   );
 }
 
 export default styled(SelectParent)`
-  .smallerMargin {
-    margin-top: 0;
-    margin-bottom: 10px;
-  }
-
-  .disableArea {
-    opacity: 1;
-
-    .disabled {
-      opacity: 0.2;
-      pointer-events: none;
-    }
-  }
 `;
diff --git a/packages/extension-ui/src/components/Address.tsx b/packages/extension-ui/src/components/Address.tsx
index 5bfd6906..641553c3 100644
--- a/packages/extension-ui/src/components/Address.tsx
+++ b/packages/extension-ui/src/components/Address.tsx
@@ -25,12 +25,13 @@ import useToast from '../hooks/useToast';
 import useTranslation from '../hooks/useTranslation';
 import { showAccount } from '../messaging';
 import { DEFAULT_TYPE } from '../util/defaultType';
+import getParentNameSuri from '../util/getParentNameSuri';
 import { AccountContext, SettingsContext } from './contexts';
 import Identicon from './Identicon';
 import Menu from './Menu';
 import Svg from './Svg';
 
-interface Props {
+export interface Props {
   actions?: React.ReactNode;
   address?: string | null;
   children?: React.ReactNode;
@@ -167,7 +168,7 @@ function Address ({ actions, address, children, className, genesisHash, isExtern
       </>);
   };
 
-  const parentNameSuri = `${parentName || ''}  ${suri || ''}`;
+  const parentNameSuri = getParentNameSuri(parentName, suri);
 
   return (
     <div className={className}>
@@ -221,7 +222,7 @@ function Address ({ actions, address, children, className, genesisHash, isExtern
                   : undefined
               }
             >
-              {chain.name.replace('Relay Chain', '')}
+              {chain.name.replace(' Relay Chain', '')}
             </div>
           )}
           <div className='addressDisplay'>
diff --git a/packages/extension-ui/src/partials/MenuAdd.tsx b/packages/extension-ui/src/partials/MenuAdd.tsx
index 75303537..0114d160 100644
--- a/packages/extension-ui/src/partials/MenuAdd.tsx
+++ b/packages/extension-ui/src/partials/MenuAdd.tsx
@@ -3,7 +3,7 @@
 
 import type { ThemeProps } from '../types';
 
-import { faFileUpload, faKey, faPlusCircle, faQrcode } from '@fortawesome/free-solid-svg-icons';
+import { faCodeBranch, faFileUpload, faKey, faPlusCircle, faQrcode } from '@fortawesome/free-solid-svg-icons';
 import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 import React, { useCallback, useContext } from 'react';
 import styled from 'styled-components';
@@ -36,16 +36,23 @@ function MenuAdd ({ className, reference }: Props): React.ReactElement<Props> {
       reference={reference}
     >
       <MenuItem className='menuItem'>
-        <Link to={master ? `/account/derive/${master.address}` : '/account/create'}>
+        <Link to={'/account/create'}>
           <FontAwesomeIcon icon={faPlusCircle} />
-          <span>{
-            master
-              ? t('Create new account (root or derived)')
-              : t('Create new account')
-          }</span>
+          <span>{ t('Create new account')}</span>
         </Link>
       </MenuItem>
       <MenuDivider />
+      {!!master && (
+        <>
+          <MenuItem className='menuItem'>
+            <Link to={`/account/derive/${master.address}`}>
+              <FontAwesomeIcon icon={faCodeBranch} />
+              <span>{t('Derive from an account')}</span>
+            </Link>
+          </MenuItem>
+          <MenuDivider />
+        </>
+      )}
       <MenuItem className='menuItem'>
         <Link to='/account/import-seed'>
           <FontAwesomeIcon icon={faKey} />
diff --git a/packages/extension-ui/src/util/getParentNameSuri.ts b/packages/extension-ui/src/util/getParentNameSuri.ts
new file mode 100644
index 00000000..3b907ca7
--- /dev/null
+++ b/packages/extension-ui/src/util/getParentNameSuri.ts
@@ -0,0 +1,6 @@
+// Copyright 2019-2020 @polkadot/extension-ui authors & contributors
+// SPDX-License-Identifier: Apache-2.0
+
+export default function (parentName?: string | null, suri?: string): string {
+  return `${parentName || ''}  ${suri || ''}`;
+}
diff --git a/packages/extension/package.json b/packages/extension/package.json
index 850ba93b..2e5e899b 100644
--- a/packages/extension/package.json
+++ b/packages/extension/package.json
@@ -1,14 +1,14 @@
 {
   "name": "@polkadot/extension",
   "description": "A sample signer extension for the @polkadot/api",
-  "version": "0.35.2-19",
+  "version": "0.35.2-22",
   "author": "Jaco Greeff <jacogr@gmail.com>",
   "license": "Apache-2",
   "dependencies": {
     "@babel/runtime": "^7.12.5",
-    "@polkadot/extension-base": "^0.35.2-19",
-    "@polkadot/extension-inject": "^0.35.2-19",
-    "@polkadot/extension-ui": "^0.35.2-19"
+    "@polkadot/extension-base": "^0.35.2-22",
+    "@polkadot/extension-inject": "^0.35.2-22",
+    "@polkadot/extension-ui": "^0.35.2-22"
   },
   "devDependencies": {
     "@polkadot/dev": "^0.61.8",
diff --git a/yarn.lock b/yarn.lock
index 950fdb8d..8d289276 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1969,13 +1969,13 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@polkadot/extension-base@^0.35.2-19, @polkadot/extension-base@workspace:packages/extension-base":
+"@polkadot/extension-base@^0.35.2-22, @polkadot/extension-base@workspace:packages/extension-base":
   version: 0.0.0-use.local
   resolution: "@polkadot/extension-base@workspace:packages/extension-base"
   dependencies:
     "@babel/runtime": ^7.12.5
     "@polkadot/api": ^3.0.2-8
-    "@polkadot/extension-inject": ^0.35.2-19
+    "@polkadot/extension-inject": ^0.35.2-22
     "@polkadot/keyring": ^5.0.2-7
     "@polkadot/phishing": ^0.3.1
     "@polkadot/ui-keyring": ^0.63.2-2
@@ -1998,7 +1998,7 @@ __metadata:
   resolution: "@polkadot/extension-dapp@workspace:packages/extension-dapp"
   dependencies:
     "@babel/runtime": ^7.12.5
-    "@polkadot/extension-inject": ^0.35.2-19
+    "@polkadot/extension-inject": ^0.35.2-22
     "@polkadot/util": ^5.0.2-7
     "@polkadot/util-crypto": ^5.0.2-7
   peerDependencies:
@@ -2006,7 +2006,7 @@ __metadata:
   languageName: unknown
   linkType: soft
 
-"@polkadot/extension-inject@^0.35.2-19, @polkadot/extension-inject@workspace:packages/extension-inject":
+"@polkadot/extension-inject@^0.35.2-22, @polkadot/extension-inject@workspace:packages/extension-inject":
   version: 0.0.0-use.local
   resolution: "@polkadot/extension-inject@workspace:packages/extension-inject"
   dependencies:
@@ -2018,7 +2018,7 @@ __metadata:
   languageName: unknown
   linkType: soft
 
-"@polkadot/extension-ui@^0.35.2-19, @polkadot/extension-ui@workspace:packages/extension-ui":
+"@polkadot/extension-ui@^0.35.2-22, @polkadot/extension-ui@workspace:packages/extension-ui":
   version: 0.0.0-use.local
   resolution: "@polkadot/extension-ui@workspace:packages/extension-ui"
   dependencies:
@@ -2027,8 +2027,8 @@ __metadata:
     "@fortawesome/free-regular-svg-icons": ^5.15.1
     "@fortawesome/free-solid-svg-icons": ^5.15.1
     "@fortawesome/react-fontawesome": ^0.1.13
-    "@polkadot/extension-base": ^0.35.2-19
-    "@polkadot/extension-inject": ^0.35.2-19
+    "@polkadot/extension-base": ^0.35.2-22
+    "@polkadot/extension-inject": ^0.35.2-22
     "@polkadot/keyring": ^5.0.2-7
     "@polkadot/react-identicon": ^0.63.2-2
     "@polkadot/react-qr": ^0.63.2-2
@@ -2065,9 +2065,9 @@ __metadata:
   dependencies:
     "@babel/runtime": ^7.12.5
     "@polkadot/dev": ^0.61.8
-    "@polkadot/extension-base": ^0.35.2-19
-    "@polkadot/extension-inject": ^0.35.2-19
-    "@polkadot/extension-ui": ^0.35.2-19
+    "@polkadot/extension-base": ^0.35.2-22
+    "@polkadot/extension-inject": ^0.35.2-22
+    "@polkadot/extension-ui": ^0.35.2-22
     babel-loader: ^8.2.2
     browser-resolve: ^2.0.0
     buffer: ^6.0.3


Test patch:
diff --git a/packages/extension-ui/src/Popup/CreateAccount/CreateAccount.test.tsx b/packages/extension-ui/src/Popup/CreateAccount/CreateAccount.test.tsx
index 1bc9f68c..bc09d6ed 100644
--- a/packages/extension-ui/src/Popup/CreateAccount/CreateAccount.test.tsx
+++ b/packages/extension-ui/src/Popup/CreateAccount/CreateAccount.test.tsx
@@ -79,6 +79,12 @@ describe('Create Account', () => {
       expect(onActionStub).toBeCalledWith('/');
     });
 
+    it('checking the checkbox enables the Next button', () => {
+      check(wrapper.find('input[type="checkbox"]'));
+
+      expect(wrapper.find(Button).prop('isDisabled')).toBe(false);
+    });
+
     it('clicking on Next activates phase 2', () => {
       check(wrapper.find('input[type="checkbox"]'));
       wrapper.find('button').simulate('click');
@@ -151,14 +157,14 @@ describe('Create Account', () => {
       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 () => {
+    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 visible when both passwords are equal', async () => {
+    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);
@@ -189,19 +195,19 @@ describe('Create Account', () => {
       expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(true);
     });
 
-    it('first password changes - button is not disabled', async () => {
+    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 visible', async () => {
+    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 visible', async () => {
+    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);
diff --git a/packages/extension-ui/src/Popup/Derive/Derive.test.tsx b/packages/extension-ui/src/Popup/Derive/Derive.test.tsx
index fe0c931c..dba2a584 100644
--- a/packages/extension-ui/src/Popup/Derive/Derive.test.tsx
+++ b/packages/extension-ui/src/Popup/Derive/Derive.test.tsx
@@ -27,6 +27,7 @@ const accounts = [
   { address: '5GYmFzQCuC5u3tQNiMZNbFGakrz3Jq31NmMg4D2QAkSoQ2g5', 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' },
   { address: '5EeaoDj4VDk8V6yQngKBaCD5MpJUCHrhYjVhBjgMHXoYon1s', isExternal: false, name: 'D', type: 'sr25519' },
   { address: '5HRKYp5anSNGtqC7cq9ftiaq4y8Mk7uHk7keaXUrQwZqDWLJ', name: 'DD', parentAddress: '5EeaoDj4VDk8V6yQngKBaCD5MpJUCHrhYjVhBjgMHXoYon1s', type: 'sr25519' }
 ] as AccountJson[];
@@ -63,8 +64,6 @@ describe('Derive', () => {
   let wrapper: ReactWrapper;
   let onActionStub: jest.Mock;
 
-  const uncheck = (input: ReactWrapper): unknown => input.simulate('change', { target: { checked: false } });
-
   const type = async (input: ReactWrapper, value: string): Promise<void> => {
     input.simulate('change', { target: { value } });
     await act(flushAllPromises);
@@ -90,48 +89,25 @@ describe('Derive', () => {
       return {} as ResponseDeriveValidate;
     });
 
-    it('"Derive" checkbox is selected by default', () => {
-      const checkbox = wrapper.find('input[type="checkbox"]');
-
-      expect(checkbox.prop('checked')).toBe(true);
-    });
-
-    it('"Create new root" button becomes visible after checkbox is unchecked', () => {
-      uncheck(wrapper.find('input[type="checkbox"]'));
-
-      expect(wrapper.exists('[data-button-action="create root account"]')).toBe(true);
-      expect(wrapper.exists('[data-button-action="create derived account"]')).toBe(false);
-    });
-
-    it('Password entry is hidden after checkbox is unchecked', () => {
-      uncheck(wrapper.find('input[type="checkbox"]'));
+    it('Button is disabled and password field visible', () => {
+      const button = wrapper.find('[data-button-action="create derived account"] button');
 
-      expect(wrapper.exists('input[type="password"]')).toBe(false);
+      expect(button.exists()).toBe(true);
+      expect(button.prop('disabled')).toBe(true);
     });
 
-    it('"Create new root" button redirects to /account/create', () => {
-      uncheck(wrapper.find('input[type="checkbox"]'));
-      wrapper.find('[data-button-action="create root account"] button').simulate('click');
+    it('Password field is visible and not in error state', () => {
+      const passwordField = wrapper.find('[data-input-password]').first();
 
-      expect(onActionStub).toBeCalledWith('/account/create');
-    });
-
-    it('"Create derived account" is visible when checkbox is checked', () => {
-      expect(wrapper.exists('[data-button-action="create derived account"]')).toBe(true);
-      expect(wrapper.exists('[data-button-action="create root account"]')).toBe(false);
+      expect(passwordField.exists()).toBe(true);
+      expect(passwordField.prop('isError')).toBe(false);
     });
 
     it('No error is visible when first loading the page', () => {
       expect(wrapper.find('Warning')).toHaveLength(0);
     });
 
-    it('"Create derived account" is disabled when password is not set', () => {
-      const button = wrapper.find('[data-button-action="create derived account"] button');
-
-      expect(button.prop('disabled')).toBe(true);
-    });
-
-    it('An error is visible and "Create derived account" is disabled when password is incorrect', async () => {
+    it('An error is visible, input higlighted and the button disabled when password is incorrect', async () => {
       await type(wrapper.find('input[type="password"]'), 'wrong_pass');
       wrapper.find('[data-button-action="create derived account"] button').simulate('click');
       await act(flushAllPromises);
@@ -140,6 +116,7 @@ describe('Derive', () => {
       const button = wrapper.find('[data-button-action="create derived account"] button');
 
       expect(button.prop('disabled')).toBe(true);
+      expect(wrapper.find('[data-input-password]').first().prop('isError')).toBe(true);
       expect(wrapper.find('.warning-message')).toHaveLength(1);
       expect(wrapper.find('.warning-message').first().text()).toEqual('Wrong password');
     });
@@ -155,10 +132,11 @@ describe('Derive', () => {
       const button = wrapper.find('[data-button-action="create derived account"] button');
 
       expect(button.prop('disabled')).toBe(false);
+      expect(wrapper.find('[data-input-password]').first().prop('isError')).toBe(false);
       expect(wrapper.find('.warning-message')).toHaveLength(0);
     });
 
-    it('"Create derived account" is enabled when password is set', async () => {
+    it('Button is enabled when password is set', async () => {
       await type(wrapper.find('input[type="password"]'), 'pass');
 
       const button = wrapper.find('[data-button-action="create derived account"] button');
@@ -167,7 +145,7 @@ describe('Derive', () => {
       expect(wrapper.find('.warning-message')).toHaveLength(0);
     });
 
-    it('An error is visible and "Create derived account" is disabled when suri is incorrect', async () => {
+    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('[data-input-suri] input'), '//');
       wrapper.find('[data-button-action="create derived account"] button').simulate('click');
@@ -199,7 +177,7 @@ describe('Derive', () => {
       expect(wrapper.find('[data-field="name"]').first().text()).toBe('B');
     });
 
-    it('selects internal root accounts as other options', () => {
+    it('selects internal root accounts as other options, no external and no Ethereum account', () => {
       const options = wrapper.find('[data-parent-option] [data-field="name"]').map((el) => el.text());
 
       expect(options).toEqual(['A', 'B', 'D']);
@@ -217,10 +195,6 @@ describe('Derive', () => {
       wrapper = (await mountComponent(true)).wrapper;
     });
 
-    it('checkbox does not exist', () => {
-      expect(wrapper.exists('[type="checkbox"]')).toBe(false);
-    });
-
     it('address dropdown does not exist', () => {
       expect(wrapper.exists(AddressDropdown)).toBe(false);
     });
diff --git a/packages/extension-ui/src/components/Address.test.tsx b/packages/extension-ui/src/components/Address.test.tsx
new file mode 100644
index 00000000..0146b4f0
--- /dev/null
+++ b/packages/extension-ui/src/components/Address.test.tsx
@@ -0,0 +1,308 @@
+// Copyright 2019-2020 @polkadot/extension-ui authors & contributors
+// SPDX-License-Identifier: Apache-2.0
+
+import '../../../../__mocks__/chrome';
+
+import type { ReactWrapper } from 'enzyme';
+import type { AccountJson } from '@polkadot/extension-base/background/types';
+import type { IconTheme } from '@polkadot/react-identicon/types';
+import type { Props as AddressComponentProps } from './Address';
+
+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 * as messaging from '../messaging';
+import * as MetadataCache from '../MetadataCache';
+import { westendMetadata } from '../Popup/Signing/metadataMock';
+import { flushAllPromises } from '../testHelpers';
+import { buildHierarchy } from '../util/buildHierarchy';
+import { DEFAULT_TYPE } from '../util/defaultType';
+import getParentNameSuri from '../util/getParentNameSuri';
+import { AccountContext, Address } from '.';
+
+// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-call
+configure({ adapter: new Adapter() });
+
+interface AccountTestJson extends AccountJson {
+  expectedIconTheme: IconTheme
+}
+
+interface AccountTestGenesisJson extends AccountTestJson {
+  expectedEncodedAddress: string;
+  expectedNetworkLabel: string;
+  genesisHash: string;
+}
+
+const externalAccount = { address: '5EeaoDj4VDk8V6yQngKBaCD5MpJUCHrhYjVhBjgMHXoYon1s', expectedIconTheme: 'polkadot', isExternal: true, name: 'External Account', type: 'sr25519' } as AccountJson;
+
+const accounts = [
+  { address: '5HSDXAC3qEMkSzZK377sTD1zJhjaPiX5tNWppHx2RQMYkjaJ', expectedIconTheme: 'polkadot', name: 'ECDSA Account', type: 'ecdsa' },
+  { address: '5FjgD3Ns2UpnHJPVeRViMhCttuemaRXEqaD8V5z4vxcsUByA', expectedIconTheme: 'polkadot', name: 'Ed Account', type: 'ed25519' },
+  { address: '5Ggap6soAPaP5UeNaiJsgqQwdVhhNnm6ez7Ba1w9jJ62LM2Q', expectedIconTheme: 'polkadot', name: 'Parent Sr Account', type: 'sr25519' },
+  { address: '0xd5D81CD4236a43F48A983fc5B895975c511f634D', expectedIconTheme: 'ethereum', name: 'Ethereum', type: 'ethereum' },
+  { ...externalAccount }
+] as AccountTestJson[];
+
+// With Westend genesis Hash
+// This account isn't part of the generic test because Westend isn't a built in network
+// The network would only be displayed if the corresponding metadata are known
+const westEndAccount = {
+  address: 'Cs2LLqQ6DSRx8UPdVp6jny4DvwNqziBSowSu5Nb1u3R6Z7X',
+  expectedEncodedAddress: '5CMQg2VXTrRWCUewro13qqc45Lf93KtzzS6hWR6dY6pvMZNF',
+  expectedIconTheme: 'polkadot',
+  expectedNetworkLabel: 'Westend',
+  genesisHash: '0xe143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e',
+  name: 'acc',
+  type: 'ed25519'
+} as AccountTestGenesisJson;
+
+const accountsWithGenesisHash = [
+  // with Polkadot genesis Hash
+  {
+    address: '5Ggap6soAPaP5UeNaiJsgqQwdVhhNnm6ez7Ba1w9jJ62LM2Q',
+    expectedEncodedAddress: '15csxS8s2AqrX1etYMMspzF6V7hM56KEjUqfjJvWHP7YWkoF',
+    expectedIconTheme: 'polkadot',
+    expectedNetworkLabel: 'Polkadot',
+    genesisHash: '0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3',
+    type: 'sr25519'
+  },
+  // with Kusama genesis Hash
+  {
+    address: '5DoYawpxt6aBy1pKAt1beLMrakqtbWMtG3NF6jwRR8uKJGqD',
+    expectedEncodedAddress: 'EKAFGAqWTb7ifdkwapeYHirjM88QBB4iRCzVQDNtw7p3bgF',
+    expectedIconTheme: 'polkadot',
+    expectedNetworkLabel: 'Kusama',
+    genesisHash: '0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe',
+    type: 'sr25519'
+  },
+  // with Edgeware genesis Hash
+  {
+    address: '5GYQRJj3NUznYDzCduENRcocMsyxmb6tjb5xW87ZMErBe9R7',
+    expectedEncodedAddress: 'mzKNamvvJPM5ApxwGSYD5VjjtyfrB4g8fhMyCc29K37nuop',
+    expectedIconTheme: 'substrate',
+    expectedNetworkLabel: 'Edgeware',
+    genesisHash: '0x742a2ca70c2fda6cee4f8df98d64c4c670a052d9568058982dad9d5a7a135c5b',
+    type: 'sr25519'
+  }
+] as AccountTestGenesisJson[];
+
+const mountComponent = async (addressComponentProps: AddressComponentProps, contextAccounts: AccountJson[]): Promise<{
+  wrapper: ReactWrapper;
+}> => {
+  const actionStub = jest.fn();
+  const { actions = actionStub } = addressComponentProps;
+
+  const wrapper = mount(
+    <AccountContext.Provider value={{
+      accounts: contextAccounts,
+      hierarchy: buildHierarchy(contextAccounts)
+    }}>
+      <Address
+        actions={actions}
+        {...addressComponentProps}
+      />
+    </AccountContext.Provider>
+  );
+
+  await act(flushAllPromises);
+  wrapper.update();
+
+  return { wrapper };
+};
+
+const getWrapper = async (account: AccountJson, contextAccounts : AccountJson[], withAccountsInContext: boolean) => {
+  // the address component can query info about the account from the account context
+  // in this case, the account's address (any encoding) should suffice
+  // In case the account is not in the context, then more info are needed as props
+  // to display accurately
+  const mountedComponent = withAccountsInContext
+  // only the address is passed as props, the full acount info are loaded in the context
+    ? await mountComponent({ address: account.address }, contextAccounts)
+  // the context is empty, all account's info are passed as props to the Address component
+    : await mountComponent(account, []);
+
+  return mountedComponent.wrapper;
+};
+
+const genericTestSuite = (account: AccountTestJson, withAccountsInContext = true) => {
+  let wrapper: ReactWrapper;
+  const { address, expectedIconTheme, name = '', type = DEFAULT_TYPE } = account;
+
+  describe(`Account ${withAccountsInContext ? 'in context from address' : 'from props'} (${name}) - ${type}`, () => {
+    beforeAll(async () => {
+      wrapper = await getWrapper(account, accounts, withAccountsInContext);
+    });
+
+    it('shows the account address and name', () => {
+      expect(wrapper.find('[data-field="address"]').text()).toEqual(address);
+      expect(wrapper.find('Name span').text()).toEqual(name);
+    });
+
+    it(`shows a ${expectedIconTheme} identicon`, () => {
+      expect(wrapper.find('Identicon').first().prop('iconTheme')).toEqual(expectedIconTheme);
+    });
+
+    it('can copy its address', () => {
+      // the first CopyToClipboard is from the identicon, the second from the copy button
+      expect(wrapper.find('CopyToClipboard').at(0).prop('text')).toEqual(address);
+      expect(wrapper.find('CopyToClipboard').at(1).prop('text')).toEqual(address);
+    });
+
+    it('has the account visiblity icon', () => {
+      expect(wrapper.find('FontAwesomeIcon.visibleIcon')).toHaveLength(1);
+    });
+
+    it('can hide the account', () => {
+      jest.spyOn(messaging, 'showAccount').mockResolvedValue(false);
+
+      const visibleIcon = wrapper.find('FontAwesomeIcon.visibleIcon');
+      const hiddenIcon = wrapper.find('FontAwesomeIcon.hiddenIcon');
+
+      expect(visibleIcon.exists()).toBe(true);
+      expect(hiddenIcon.exists()).toBe(false);
+
+      visibleIcon.simulate('click');
+      expect(messaging.showAccount).toBeCalledWith(address, false);
+    });
+
+    it('can show the account if hidden', async () => {
+      const additionalProps = { isHidden: true };
+
+      const mountedHiddenComponent = withAccountsInContext
+        ? await mountComponent({ address, ...additionalProps }, accounts)
+        : await mountComponent({ ...account, ...additionalProps }, []);
+
+      const wrapperHidden = mountedHiddenComponent.wrapper;
+
+      jest.spyOn(messaging, 'showAccount').mockResolvedValue(true);
+
+      const visibleIcon = wrapperHidden.find('FontAwesomeIcon.visibleIcon');
+      const hiddenIcon = wrapperHidden.find('FontAwesomeIcon.hiddenIcon');
+
+      expect(visibleIcon.exists()).toBe(false);
+      expect(hiddenIcon.exists()).toBe(true);
+
+      hiddenIcon.simulate('click');
+      expect(messaging.showAccount).toBeCalledWith(address, true);
+    });
+
+    it('has settings button', () => {
+      expect(wrapper.find('.settings')).toHaveLength(1);
+    });
+
+    it('has no account hidding and settings button if no action is provided', async () => {
+      const additionalProps = { actions: null };
+
+      const mountedComponentWithoutAction = withAccountsInContext
+        ? await mountComponent({ address, ...additionalProps }, accounts)
+        : await mountComponent({ ...account, ...additionalProps }, []);
+
+      wrapper = mountedComponentWithoutAction.wrapper;
+
+      expect(wrapper.find('.settings')).toHaveLength(0);
+    });
+  });
+};
+
+const genesisHashTestSuite = (account: AccountTestGenesisJson, withAccountsInContext = true) => {
+  const { expectedEncodedAddress, expectedIconTheme, expectedNetworkLabel } = account;
+
+  describe(`Account ${withAccountsInContext ? 'in context from address' : 'from props'} with ${expectedNetworkLabel} genesiHash`, () => {
+    let wrapper: ReactWrapper;
+
+    beforeAll(async () => {
+      wrapper = await getWrapper(account, accountsWithGenesisHash, withAccountsInContext);
+    });
+
+    it('shows the account address correctly encoded', () => {
+      expect(wrapper.find('[data-field="address"]').text()).toEqual(expectedEncodedAddress);
+    });
+
+    it(`shows a ${expectedIconTheme} identicon`, () => {
+      expect(wrapper.find('Identicon').first().prop('iconTheme')).toEqual(expectedIconTheme);
+    });
+
+    it('Copy buttons contain the encoded address', () => {
+      // the first CopyToClipboard is from the identicon, the second from the copy button
+      expect(wrapper.find('CopyToClipboard').at(0).prop('text')).toEqual(expectedEncodedAddress);
+      expect(wrapper.find('CopyToClipboard').at(1).prop('text')).toEqual(expectedEncodedAddress);
+    });
+
+    it('Network label shows the correct network', () => {
+      expect(wrapper.find('[data-field="chain"]').text()).toEqual(expectedNetworkLabel);
+    });
+  });
+};
+
+describe('Address', () => {
+  accounts.forEach((account) => {
+    genericTestSuite(account);
+    genericTestSuite(account, false);
+  });
+
+  accountsWithGenesisHash.forEach((account) => {
+    genesisHashTestSuite(account);
+    genesisHashTestSuite(account, false);
+  });
+
+  describe('External account', () => {
+    let wrapper: ReactWrapper;
+
+    beforeAll(async () => {
+      wrapper = await getWrapper(externalAccount, [], false);
+    });
+
+    it('has an icon in front of its name', () => {
+      expect(wrapper.find('Name').find('FontAwesomeIcon [data-icon="external-link-square-alt"]').exists()).toBe(true);
+    });
+  });
+
+  describe('Encoding and label based on Metadata', () => {
+    let wrapper: ReactWrapper;
+
+    beforeAll(async () => {
+      jest.spyOn(MetadataCache, 'getSavedMeta').mockResolvedValue(westendMetadata);
+
+      wrapper = await getWrapper(westEndAccount, [], false);
+    });
+
+    it('shows westend label with the correct color', () => {
+      const bannerChain = wrapper.find('[data-field="chain"]');
+
+      expect(bannerChain.text()).toEqual(westendMetadata.chain);
+      expect(bannerChain.prop('style')?.backgroundColor).toEqual(westendMetadata.color);
+    });
+
+    it('shows the account correctly reencoded', () => {
+      expect(wrapper.find('[data-field="address"]').text()).toEqual(westEndAccount.expectedEncodedAddress);
+    });
+  });
+
+  describe('Derived accounts', () => {
+    let wrapper: ReactWrapper;
+    const childAccount = {
+      address: '5Ggap6soAPaP5UeNaiJsgqQwdVhhNnm6ez7Ba1w9jJ62LM2Q',
+      name: 'Luke',
+      parentName: 'Dark Vador',
+      suri: '//42',
+      type: 'sr25519'
+    } as AccountJson;
+
+    beforeAll(async () => {
+      wrapper = await getWrapper(childAccount, [], false);
+    });
+
+    it('shows the child\'s account address and name', () => {
+      expect(wrapper.find('[data-field="address"]').text()).toEqual(childAccount.address);
+      expect(wrapper.find('Name span').text()).toEqual(childAccount.name);
+    });
+
+    it('shows the parent account and suri', () => {
+      const expectedParentNameSuri = getParentNameSuri(childAccount.parentName as string, childAccount.suri);
+
+      expect(wrapper.find('.parentName').text()).toEqual(expectedParentNameSuri);
+    });
+  });
+});