Repository: polkadot-js/extension
Pull request #617 (2021-01-08 08:33:01)

Add Web3AccountsOptions to web3FromAddress function

jacogr: Happy to accept a PR like discussed above :) (Can just re-open and make the changes?)

Patch:
diff --git a/__mocks__/react-i18next.ts b/__mocks__/react-i18next.ts
new file mode 100644
index 00000000..68ac12e3
--- /dev/null
+++ b/__mocks__/react-i18next.ts
@@ -0,0 +1,24 @@
+// Copyright 2019-2021 @polkadot/extension-ui authors & contributors
+// SPDX-License-Identifier: Apache-2.0
+
+import React from 'react';
+
+interface useTranslationReturnObj {
+  i18n: { changeLanguage: () => Promise<unknown>; };
+  t: (str: string) => string;
+}
+
+export const useTranslation = (): useTranslationReturnObj => {
+  return {
+    i18n: {
+      changeLanguage: () => new Promise(() => { /**/ })
+    },
+    t: (str: string) => str
+  };
+};
+
+export const withTranslation = () => (component: React.ReactElement): React.ReactElement => component;
+
+export const Trans = ({ children }: { children: React.ReactElement }): React.ReactElement => children;
+
+export default withTranslation;
diff --git a/package.json b/package.json
index f66504fe..2809b186 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.36.2-0",
+  "version": "0.36.2-3",
   "private": true,
   "workspaces": [
     "packages/*"
diff --git a/packages/extension-base/package.json b/packages/extension-base/package.json
index 7ed035b7..8ab0b6da 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.36.2-0",
+  "version": "0.36.2-3",
   "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.3.1",
-    "@polkadot/extension-inject": "^0.36.2-0",
+    "@polkadot/extension-inject": "^0.36.2-3",
     "@polkadot/keyring": "^5.2.3",
     "@polkadot/phishing": "^0.3.7",
     "@polkadot/ui-keyring": "^0.65.2-0"
diff --git a/packages/extension-chains/package.json b/packages/extension-chains/package.json
index b2361205..743b6a7d 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.36.2-0",
+  "version": "0.36.2-3",
   "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 d1d391ff..c0587c70 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.36.2-0",
+  "version": "0.36.2-3",
   "author": "Jaco Greeff <jacogr@gmail.com>",
   "license": "Apache-2",
   "main": "index.js",
   "sideEffects": false,
   "dependencies": {
     "@babel/runtime": "^7.12.5",
-    "@polkadot/extension-inject": "^0.36.2-0",
+    "@polkadot/extension-inject": "^0.36.2-3",
     "@polkadot/util": "^5.2.3",
     "@polkadot/util-crypto": "^5.2.3"
   },
diff --git a/packages/extension-dapp/src/index.ts b/packages/extension-dapp/src/index.ts
index 2f629fa8..8b856577 100644
--- a/packages/extension-dapp/src/index.ts
+++ b/packages/extension-dapp/src/index.ts
@@ -3,6 +3,7 @@
 
 import type { Injected, InjectedAccount, InjectedAccountWithMeta, InjectedExtension, InjectedExtensionInfo, InjectedProviderWithMeta, InjectedWindow, ProviderList, Unsubcall, Web3AccountsOptions } from '@polkadot/extension-inject/types';
 
+import { u8aEq } from '@polkadot/util';
 import { decodeAddress, encodeAddress } from '@polkadot/util-crypto';
 
 import { documentReadyPromise } from './util';
@@ -184,7 +185,13 @@ export async function web3FromAddress (address: string): Promise<InjectedExtensi
   }
 
   const accounts = await web3Accounts();
-  const found = address && accounts.find((account): boolean => account.address === address);
+  let found: InjectedAccountWithMeta | undefined;
+
+  if (address) {
+    const accountU8a = decodeAddress(address);
+
+    found = accounts.find((account): boolean => u8aEq(decodeAddress(account.address), accountU8a));
+  }
 
   if (!found) {
     throw new Error(`web3FromAddress: Unable to find injected ${address}`);
diff --git a/packages/extension-inject/package.json b/packages/extension-inject/package.json
index dfcf51da..e009a472 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.36.2-0",
+  "version": "0.36.2-3",
   "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 2be04e80..38f085d1 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.36.2-0",
+  "version": "0.36.2-3",
   "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.36.2-0",
-    "@polkadot/extension-inject": "^0.36.2-0",
+    "@polkadot/extension-base": "^0.36.2-3",
+    "@polkadot/extension-inject": "^0.36.2-3",
     "@polkadot/keyring": "^5.2.3",
     "@polkadot/react-identicon": "^0.65.2-0",
     "@polkadot/react-qr": "^0.65.2-0",
diff --git a/packages/extension-ui/src/Popup/CreateAccount/index.tsx b/packages/extension-ui/src/Popup/CreateAccount/index.tsx
index 426814c6..c990060c 100644
--- a/packages/extension-ui/src/Popup/CreateAccount/index.tsx
+++ b/packages/extension-ui/src/Popup/CreateAccount/index.tsx
@@ -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}
diff --git a/packages/extension-ui/src/Popup/Derive/SelectParent.tsx b/packages/extension-ui/src/Popup/Derive/SelectParent.tsx
index d7d3f449..6ef92761 100644
--- a/packages/extension-ui/src/Popup/Derive/SelectParent.tsx
+++ b/packages/extension-ui/src/Popup/Derive/SelectParent.tsx
@@ -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)`
-`;
diff --git a/packages/extension-ui/src/Popup/Derive/index.tsx b/packages/extension-ui/src/Popup/Derive/index.tsx
index 9fc0556b..1a22926c 100644
--- a/packages/extension-ui/src/Popup/Derive/index.tsx
+++ b/packages/extension-ui/src/Popup/Derive/index.tsx
@@ -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>
         </>
       )}
     </>
diff --git a/packages/extension-ui/src/Popup/ImportQr.tsx b/packages/extension-ui/src/Popup/ImportQr.tsx
index 88169a97..4d507210 100644
--- a/packages/extension-ui/src/Popup/ImportQr.tsx
+++ b/packages/extension-ui/src/Popup/ImportQr.tsx
@@ -62,15 +62,14 @@ export default function ImportQr (): React.ReactElement {
             value={name || ''}
           />
           <VerticalSpace />
-          {name && (
-            <ButtonArea>
-              <NextStepButton
-                onClick={_onCreate}
-              >
-                {t<string>('Add the account with identified address')}
-              </NextStepButton>
-            </ButtonArea>
-          )}
+          <ButtonArea>
+            <NextStepButton
+              isDisabled={!name}
+              onClick={_onCreate}
+            >
+              {t<string>('Add the account with identified address')}
+            </NextStepButton>
+          </ButtonArea>
         </>
       )}
     </>
diff --git a/packages/extension-ui/src/Popup/ImportSeed.tsx b/packages/extension-ui/src/Popup/ImportSeed.tsx
deleted file mode 100644
index 3e1d8229..00000000
--- a/packages/extension-ui/src/Popup/ImportSeed.tsx
+++ /dev/null
@@ -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;
-  }
-`;
diff --git a/packages/extension-ui/src/Popup/ImportSeed/SeedAndPath.tsx b/packages/extension-ui/src/Popup/ImportSeed/SeedAndPath.tsx
new file mode 100644
index 00000000..05d8a7cc
--- /dev/null
+++ b/packages/extension-ui/src/Popup/ImportSeed/SeedAndPath.tsx
@@ -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;
+  }
+`);
diff --git a/packages/extension-ui/src/Popup/ImportSeed/index.tsx b/packages/extension-ui/src/Popup/ImportSeed/index.tsx
new file mode 100644
index 00000000..0f3184a7
--- /dev/null
+++ b/packages/extension-ui/src/Popup/ImportSeed/index.tsx
@@ -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;
diff --git a/packages/extension-ui/src/Popup/CreateAccount/AccountName.tsx b/packages/extension-ui/src/components/AccountNamePasswordCreation.tsx
similarity index 62%
rename from packages/extension-ui/src/Popup/CreateAccount/AccountName.tsx
rename to packages/extension-ui/src/components/AccountNamePasswordCreation.tsx
index 4704035d..89a422a4 100644
--- a/packages/extension-ui/src/Popup/CreateAccount/AccountName.tsx
+++ b/packages/extension-ui/src/components/AccountNamePasswordCreation.tsx
@@ -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);
diff --git a/packages/extension-ui/src/components/Address.tsx b/packages/extension-ui/src/components/Address.tsx
index b1e88660..e2bc4fac 100644
--- a/packages/extension-ui/src/components/Address.tsx
+++ b/packages/extension-ui/src/components/Address.tsx
@@ -107,6 +107,8 @@ function Address ({ actions, address, children, className, genesisHash, isExtern
 
   useEffect((): void => {
     if (!address) {
+      setRecoded(defaultRecoded);
+
       return;
     }
 
diff --git a/packages/extension-ui/src/components/index.ts b/packages/extension-ui/src/components/index.ts
index c0b13270..c5339479 100644
--- a/packages/extension-ui/src/components/index.ts
+++ b/packages/extension-ui/src/components/index.ts
@@ -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';
diff --git a/packages/extension/package.json b/packages/extension/package.json
index 9eeb05d6..4c07e2ea 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.36.2-0",
+  "version": "0.36.2-3",
   "author": "Jaco Greeff <jacogr@gmail.com>",
   "license": "Apache-2",
   "dependencies": {
     "@babel/runtime": "^7.12.5",
-    "@polkadot/extension-base": "^0.36.2-0",
-    "@polkadot/extension-inject": "^0.36.2-0",
-    "@polkadot/extension-ui": "^0.36.2-0"
+    "@polkadot/extension-base": "^0.36.2-3",
+    "@polkadot/extension-inject": "^0.36.2-3",
+    "@polkadot/extension-ui": "^0.36.2-3"
   },
   "devDependencies": {
     "@polkadot/dev": "^0.61.22",
diff --git a/packages/extension/public/locales/en/translation.json b/packages/extension/public/locales/en/translation.json
index 30c687db..8ab39dd0 100644
--- a/packages/extension/public/locales/en/translation.json
+++ b/packages/extension/public/locales/en/translation.json
@@ -124,5 +124,11 @@
   "denied": "",
   "Derive from an account": "",
   "Dark": "",
-  "Light": ""
+  "Light": "",
+  "Invalid mnemonic seed or derivation path": "",
+  "Invalid mnemonic seed": "",
+  "Mnemonic needs to contain 12, 15, 18, 21, 24 words": "",
+  "advanced": "",
+  "derivation path": "",
+  "Next": ""
 }
diff --git a/yarn.lock b/yarn.lock
index feb4038a..19531bdf 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1920,13 +1920,13 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@polkadot/extension-base@^0.36.2-0, @polkadot/extension-base@workspace:packages/extension-base":
+"@polkadot/extension-base@^0.36.2-3, @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.3.1
-    "@polkadot/extension-inject": ^0.36.2-0
+    "@polkadot/extension-inject": ^0.36.2-3
     "@polkadot/keyring": ^5.2.3
     "@polkadot/phishing": ^0.3.7
     "@polkadot/ui-keyring": ^0.65.2-0
@@ -1949,7 +1949,7 @@ __metadata:
   resolution: "@polkadot/extension-dapp@workspace:packages/extension-dapp"
   dependencies:
     "@babel/runtime": ^7.12.5
-    "@polkadot/extension-inject": ^0.36.2-0
+    "@polkadot/extension-inject": ^0.36.2-3
     "@polkadot/util": ^5.2.3
     "@polkadot/util-crypto": ^5.2.3
   peerDependencies:
@@ -1957,7 +1957,7 @@ __metadata:
   languageName: unknown
   linkType: soft
 
-"@polkadot/extension-inject@^0.36.2-0, @polkadot/extension-inject@workspace:packages/extension-inject":
+"@polkadot/extension-inject@^0.36.2-3, @polkadot/extension-inject@workspace:packages/extension-inject":
   version: 0.0.0-use.local
   resolution: "@polkadot/extension-inject@workspace:packages/extension-inject"
   dependencies:
@@ -1969,7 +1969,7 @@ __metadata:
   languageName: unknown
   linkType: soft
 
-"@polkadot/extension-ui@^0.36.2-0, @polkadot/extension-ui@workspace:packages/extension-ui":
+"@polkadot/extension-ui@^0.36.2-3, @polkadot/extension-ui@workspace:packages/extension-ui":
   version: 0.0.0-use.local
   resolution: "@polkadot/extension-ui@workspace:packages/extension-ui"
   dependencies:
@@ -1978,8 +1978,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.36.2-0
-    "@polkadot/extension-inject": ^0.36.2-0
+    "@polkadot/extension-base": ^0.36.2-3
+    "@polkadot/extension-inject": ^0.36.2-3
     "@polkadot/keyring": ^5.2.3
     "@polkadot/react-identicon": ^0.65.2-0
     "@polkadot/react-qr": ^0.65.2-0
@@ -2016,9 +2016,9 @@ __metadata:
   dependencies:
     "@babel/runtime": ^7.12.5
     "@polkadot/dev": ^0.61.22
-    "@polkadot/extension-base": ^0.36.2-0
-    "@polkadot/extension-inject": ^0.36.2-0
-    "@polkadot/extension-ui": ^0.36.2-0
+    "@polkadot/extension-base": ^0.36.2-3
+    "@polkadot/extension-inject": ^0.36.2-3
+    "@polkadot/extension-ui": ^0.36.2-3
     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 ea2b8598..1e935e96 100644
--- a/packages/extension-ui/src/Popup/CreateAccount/CreateAccount.test.tsx
+++ b/packages/extension-ui/src/Popup/CreateAccount/CreateAccount.test.tsx
@@ -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);
-    });
-  });
 });
diff --git a/packages/extension-ui/src/Popup/Derive/Derive.test.tsx b/packages/extension-ui/src/Popup/Derive/Derive.test.tsx
index 1b99ea83..a434ce1e 100644
--- a/packages/extension-ui/src/Popup/Derive/Derive.test.tsx
+++ b/packages/extension-ui/src/Popup/Derive/Derive.test.tsx
@@ -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('/');
+      });
+    });
   });
 });
diff --git a/packages/extension-ui/src/Popup/ImportQr.test.tsx b/packages/extension-ui/src/Popup/ImportQr.test.tsx
new file mode 100644
index 00000000..9aa2a34c
--- /dev/null
+++ b/packages/extension-ui/src/Popup/ImportQr.test.tsx
@@ -0,0 +1,128 @@
+// Copyright 2019-2021 @polkadot/extension-ui authors & contributors
+// SPDX-License-Identifier: Apache-2.0
+
+import '../../../../__mocks__/chrome';
+
+import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
+import { configure, mount, ReactWrapper } from 'enzyme';
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+import { MemoryRouter } from 'react-router';
+
+import { Button } from '../components';
+import * as messaging from '../messaging';
+import { flushAllPromises } from '../testHelpers';
+import ImportQr from './ImportQr';
+
+const mockedAccount = {
+  content: '12bxf6QJS5hMJgwbJMDjFot1sq93EvgQwyuPWENr9SzJfxtN',
+  expectedBannerChain: 'Polkadot',
+  genesisHash: '0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3',
+  isAddress: true,
+  name: 'My Polkadot Account'
+};
+
+interface ScanType {
+  isAddress: boolean;
+  content: string;
+  genesisHash: string;
+  name?: string;
+}
+
+interface QrScanAddressProps {
+  className?: string;
+  onError?: (error: Error) => void;
+  onScan: (scanned: ScanType) => void;
+  size?: string | number;
+  style?: React.CSSProperties;
+}
+
+// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-call
+configure({ adapter: new Adapter() });
+
+const typeName = async (wrapper: ReactWrapper, value:string) => {
+  wrapper.find('input').first().simulate('change', { target: { value } });
+  await act(flushAllPromises);
+  wrapper.update();
+};
+
+jest.mock('@polkadot/react-qr', () => {
+  return {
+    // eslint-disable-next-line @typescript-eslint/no-unused-vars
+    QrScanAddress: ({ onScan }: QrScanAddressProps): null => {
+      return null;
+    }
+  };
+});
+
+describe('ImportQr component', () => {
+  let wrapper: ReactWrapper;
+
+  beforeEach(async () => {
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+    wrapper = mount(
+      <MemoryRouter>
+        <ImportQr />
+      </MemoryRouter>
+    );
+
+    act(() => {
+      // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
+      (wrapper.find('QrScanAddress').first().prop('onScan') as unknown as QrScanAddressProps['onScan'])(mockedAccount);
+    });
+    await act(flushAllPromises);
+    wrapper.update();
+  });
+
+  describe('Address component', () => {
+    it('shows account as external', () => {
+      expect(wrapper.find('Name').find('FontAwesomeIcon [data-icon="external-link-square-alt"]').exists()).toBe(true);
+    });
+
+    it('shows the correct name', () => {
+      expect(wrapper.find('Name span').text()).toEqual(mockedAccount.name);
+    });
+
+    it('shows the correct address', () => {
+      expect(wrapper.find('[data-field="address"]').text()).toEqual(mockedAccount.content);
+    });
+
+    it('shows the correct banner', () => {
+      expect(wrapper.find('[data-field="chain"]').text()).toEqual(mockedAccount.expectedBannerChain);
+    });
+  });
+
+  it('has the button enabled', () => {
+    expect(wrapper.find(Button).prop('isDisabled')).toBe(false);
+  });
+
+  it('displays and error and the button is disabled with a short name', async () => {
+    await typeName(wrapper, 'a');
+
+    expect(wrapper.find('.warning-message').first().text()).toBe('Account name is too short');
+    expect(wrapper.find(Button).prop('isDisabled')).toBe(true);
+  });
+
+  it('has no error message and button enabled with a long name', async () => {
+    const longName = 'aaa';
+
+    await typeName(wrapper, 'a');
+    await typeName(wrapper, longName);
+
+    expect(wrapper.find('.warning-message')).toHaveLength(0);
+    expect(wrapper.find(Button).prop('isDisabled')).toBe(false);
+    expect(wrapper.find('Name span').text()).toEqual(longName);
+  });
+
+  it('shows the external name in the input field', () => {
+    expect(wrapper.find('input').prop('value')).toBe(mockedAccount.name);
+  });
+
+  it('creates the external account', async () => {
+    jest.spyOn(messaging, 'createAccountExternal').mockResolvedValue(false);
+    wrapper.find(Button).simulate('click');
+    await act(flushAllPromises);
+
+    expect(messaging.createAccountExternal).toHaveBeenCalledWith(mockedAccount.name, mockedAccount.content, mockedAccount.genesisHash);
+  });
+});
diff --git a/packages/extension-ui/src/Popup/ImportSeed/ImportSeed.test.tsx b/packages/extension-ui/src/Popup/ImportSeed/ImportSeed.test.tsx
new file mode 100644
index 00000000..9bad09a0
--- /dev/null
+++ b/packages/extension-ui/src/Popup/ImportSeed/ImportSeed.test.tsx
@@ -0,0 +1,182 @@
+// Copyright 2019-2021 @polkadot/extension-ui authors & contributors
+// SPDX-License-Identifier: Apache-2.0
+
+import '../../../../../__mocks__/chrome';
+
+import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
+import { configure, mount, ReactWrapper } from 'enzyme';
+import React from 'react';
+import { act } from 'react-dom/test-utils';
+import { MemoryRouter } from 'react-router';
+
+import { ActionContext, Button, Warning } from '../../components';
+import * as messaging from '../../messaging';
+import { flushAllPromises } from '../../testHelpers';
+import ImportSeed from './';
+
+const account = {
+  derivation: '/1',
+  expectedAddress: '5GNg7RWeAAJuya4wTxb8aZf19bCWJroKuJNrhk4N3iYHNqTm',
+  expectedAddressWithDerivation: '5DV3x9zgaXREUMTX7GgkP3ETeW4DQAznWTxg4kx2WivGuQLQ',
+  name: 'My Polkadot Account',
+  password: 'somePassword',
+  seed: 'upgrade multiply predict hip multiply march leg devote social outer oppose debris'
+};
+
+// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-call
+configure({ adapter: new Adapter() });
+
+const typeSeed = async (wrapper: ReactWrapper, value: string) => {
+  wrapper.find('textarea').first().simulate('change', { target: { value } });
+  await act(flushAllPromises);
+  wrapper.update();
+};
+
+const typeDerivationPath = async (wrapper: ReactWrapper, value: string) => {
+  wrapper.find('input').first().simulate('change', { target: { value } });
+  await act(flushAllPromises);
+  wrapper.update();
+};
+
+describe('ImportSeed', () => {
+  let wrapper: ReactWrapper;
+  const onActionStub = jest.fn();
+
+  const type = async (input: ReactWrapper, value: string): Promise<void> => {
+    input.simulate('change', { target: { value } });
+    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);
+
+  beforeEach(async () => {
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
+    wrapper = mount(
+      <ActionContext.Provider value={onActionStub}>
+        <MemoryRouter>
+          <ImportSeed />
+        </MemoryRouter>
+      </ActionContext.Provider>
+    );
+
+    await act(flushAllPromises);
+    wrapper.update();
+  });
+
+  describe('Step 1', () => {
+    it('first shows no error, no account, and next step button is disabled', () => {
+      expect(wrapper.find('Name span').text()).toEqual('<unknown>');
+      expect(wrapper.find('[data-field="address"]').text()).toEqual('<unknown>');
+      expect(wrapper.find('.derivationPath').exists()).toBe(false);
+      expect(wrapper.find(Warning).exists()).toBe(false);
+      expect(wrapper.find(Button).prop('isDisabled')).toBe(true);
+    });
+
+    it('shows the expected account when correct seed is typed and next step button is enabled', async () => {
+      jest.spyOn(messaging, 'validateSeed').mockResolvedValue({ address: account.expectedAddress, suri: account.seed });
+      await typeSeed(wrapper, account.seed);
+
+      expect(wrapper.find(Warning).exists()).toBe(false);
+      expect(wrapper.find(Button).prop('isDisabled')).toBe(false);
+      expect(wrapper.find('Name span').text()).toEqual('<unknown>');
+      expect(wrapper.find('[data-field="address"]').text()).toEqual(account.expectedAddress);
+    });
+
+    it('shows an error when incorrect seed is typed and next step button is enabled', async () => {
+    // eslint-disable-next-line @typescript-eslint/require-await
+      jest.spyOn(messaging, 'validateSeed').mockImplementation(async () => {
+        throw new Error('Some test error message');
+      });
+      await typeSeed(wrapper, 'this is an invalid mnemonic seed');
+
+      expect(wrapper.find(Warning).find('.warning-message').text()).toEqual('Invalid mnemonic seed');
+      expect(wrapper.find(Button).prop('isDisabled')).toBe(true);
+      expect(wrapper.find('Name span').text()).toEqual('<unknown>');
+      expect(wrapper.find('[data-field="address"]').text()).toEqual('<unknown>');
+    });
+
+    it('shows an error when the seed is removed', async () => {
+      await typeSeed(wrapper, 'asdf');
+      await typeSeed(wrapper, '');
+
+      expect(wrapper.find(Warning).find('.warning-message').text()).toEqual('Mnemonic needs to contain 12, 15, 18, 21, 24 words');
+      expect(wrapper.find(Button).prop('isDisabled')).toBe(true);
+    });
+
+    it('shows the expected account with derivation when correct seed is typed and next step button is enabled', async () => {
+      const suri = `${account.seed}${account.derivation}`;
+      const validateCall = jest.spyOn(messaging, 'validateSeed').mockResolvedValue({ address: account.expectedAddressWithDerivation, suri });
+
+      await typeSeed(wrapper, account.seed);
+      wrapper.find('.advancedToggle').simulate('click');
+      await typeDerivationPath(wrapper, account.derivation);
+
+      expect(validateCall).toHaveBeenLastCalledWith(suri);
+      expect(wrapper.find(Warning).exists()).toBe(false);
+      expect(wrapper.find(Button).prop('isDisabled')).toBe(false);
+      expect(wrapper.find('Name span').text()).toEqual('<unknown>');
+      expect(wrapper.find('[data-field="address"]').text()).toEqual(account.expectedAddressWithDerivation);
+    });
+
+    it('shows an error when derivation path is incorrect and next step button is disabled', async () => {
+      const wrongPath = 'wrong';
+      const suri = `${account.seed}${wrongPath}`;
+      // eslint-disable-next-line @typescript-eslint/require-await
+      const validateCall = jest.spyOn(messaging, 'validateSeed').mockImplementation(async () => {
+        throw new Error('Some test error message');
+      });
+
+      await typeSeed(wrapper, account.seed);
+      wrapper.find('.advancedToggle').simulate('click');
+      await typeDerivationPath(wrapper, wrongPath);
+
+      expect(validateCall).toHaveBeenLastCalledWith(suri);
+      expect(wrapper.find(Warning).find('.warning-message').text()).toEqual('Invalid mnemonic seed or derivation path');
+      expect(wrapper.find(Button).prop('isDisabled')).toBe(true);
+      expect(wrapper.find('Name span').text()).toEqual('<unknown>');
+      expect(wrapper.find('[data-field="address"]').text()).toEqual('<unknown>');
+    });
+
+    it('moves to the second step', async () => {
+      jest.spyOn(messaging, 'validateSeed').mockResolvedValue({ address: account.expectedAddress, suri: account.seed });
+      await typeSeed(wrapper, account.seed);
+      wrapper.find(Button).simulate('click');
+      await act(flushAllPromises);
+      wrapper.update();
+
+      expect(wrapper.find(Button)).toHaveLength(2);
+      expect(wrapper.find('Name span').text()).toEqual('<unknown>');
+      expect(wrapper.find('[data-field="address"]').text()).toEqual(account.expectedAddress);
+    });
+
+    describe('Phase 2', () => {
+      const suri = `${account.seed}${account.derivation}`;
+
+      beforeEach(async () => {
+        jest.spyOn(messaging, 'createAccountSuri').mockResolvedValue(true);
+        jest.spyOn(messaging, 'validateSeed').mockResolvedValue({ address: account.expectedAddressWithDerivation, suri });
+
+        await typeSeed(wrapper, account.seed);
+        wrapper.find('.advancedToggle').simulate('click');
+        await typeDerivationPath(wrapper, account.derivation);
+        wrapper.find(Button).simulate('click');
+
+        await act(flushAllPromises);
+        wrapper.update();
+      });
+
+      it('saves account 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);
+        wrapper.update();
+
+        expect(messaging.createAccountSuri).toBeCalledWith(account.name, account.password, suri);
+        expect(onActionStub).toBeCalledWith('/');
+      });
+    });
+  });
+});
diff --git a/packages/extension-ui/src/components/AccountNamePasswordCreation.test.tsx b/packages/extension-ui/src/components/AccountNamePasswordCreation.test.tsx
new file mode 100644
index 00000000..e2a7b49c
--- /dev/null
+++ b/packages/extension-ui/src/components/AccountNamePasswordCreation.test.tsx
@@ -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);
+  });
+});