diff --git a/CHANGELOG.md b/CHANGELOG.md index 2802c302..fb055bf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,13 @@ # CHANGELOG -## 0.26.0-beta.11 +## 0.26.0-beta.x - Add the ability to import JSON keystore files (Thanks to https://github.com/shawntabrizi) - updated to derivation documentation (Thanks to https://github.com/EthWorks) - Adjust allowed mnemonic seed strengths, 12, 15, 18, 21 & 24 all allowed - Remove duplication with Default/Substrate prefixes in dropdown (equivalent, only generic displayed) +- Display child accounts when no parent has been found (orphans) +- Display derived suri alongside parent account names ## 0.25.1 May 14, 2020 diff --git a/packages/extension-base/src/background/handlers/Extension.ts b/packages/extension-base/src/background/handlers/Extension.ts index 0a87eebf..09bec0b0 100644 --- a/packages/extension-base/src/background/handlers/Extension.ts +++ b/packages/extension-base/src/background/handlers/Extension.ts @@ -344,7 +344,8 @@ export default class Extension { const childPair = this.derive(parentAddress, suri, parentPassword, { genesisHash, name, - parentAddress + parentAddress, + suri }); keyring.addPair(childPair, password); diff --git a/packages/extension-base/src/background/types.ts b/packages/extension-base/src/background/types.ts index 345539dc..1feb8577 100644 --- a/packages/extension-base/src/background/types.ts +++ b/packages/extension-base/src/background/types.ts @@ -29,6 +29,7 @@ export interface AccountJson extends KeyringPair$Meta { isExternal?: boolean; name?: string; parentAddress?: string; + suri?: string; whenCreated?: number; } diff --git a/packages/extension-ui/src/Popup/Accounts/Account.test.tsx b/packages/extension-ui/src/Popup/Accounts/Account.test.tsx index 87617028..584ceb3a 100644 --- a/packages/extension-ui/src/Popup/Accounts/Account.test.tsx +++ b/packages/extension-ui/src/Popup/Accounts/Account.test.tsx @@ -33,19 +33,19 @@ describe('Account component', () => { wrapper = mountAccountComponent({ isExternal: false }); wrapper.find('Details').simulate('click'); - expect(wrapper.find('MenuItem').length).toBe(4); - expect(wrapper.find('MenuItem').at(0).text()).toBe('Rename'); - expect(wrapper.find('MenuItem').at(1).text()).toBe('Derive New Account'); - expect(wrapper.find('MenuItem').at(2).text()).toBe('Export Account'); - expect(wrapper.find('MenuItem').at(3).text()).toBe('Forget Account'); + expect(wrapper.find('a.menuItem').length).toBe(4); + expect(wrapper.find('a.menuItem').at(0).text()).toBe('Rename'); + expect(wrapper.find('a.menuItem').at(1).text()).toBe('Derive New Account'); + expect(wrapper.find('a.menuItem').at(2).text()).toBe('Export Account'); + expect(wrapper.find('a.menuItem').at(3).text()).toBe('Forget Account'); }); it('does not show Export option if account is external', () => { wrapper = mountAccountComponent({ isExternal: true }); wrapper.find('Details').simulate('click'); - expect(wrapper.find('MenuItem').length).toBe(2); - expect(wrapper.find('MenuItem').at(0).text()).toBe('Rename'); - expect(wrapper.find('MenuItem').at(1).text()).toBe('Forget Account'); + expect(wrapper.find('a.menuItem').length).toBe(2); + expect(wrapper.find('a.menuItem').at(0).text()).toBe('Rename'); + expect(wrapper.find('a.menuItem').at(1).text()).toBe('Forget Account'); }); }); diff --git a/packages/extension-ui/src/Popup/Accounts/Account.tsx b/packages/extension-ui/src/Popup/Accounts/Account.tsx index d8d830d7..9861b00d 100644 --- a/packages/extension-ui/src/Popup/Accounts/Account.tsx +++ b/packages/extension-ui/src/Popup/Accounts/Account.tsx @@ -5,7 +5,7 @@ import { AccountJson } from '@polkadot/extension-base/background/types'; import { ThemeProps } from '../../types'; -import React, { useCallback, useContext, useState } from 'react'; +import React, { useCallback, useContext, useMemo, useState } from 'react'; import styled from 'styled-components'; import { ActionContext, Address, Link } from '../../components'; @@ -13,12 +13,11 @@ import { editAccount } from '../../messaging'; import { Name } from '../../partials'; interface Props extends AccountJson { - address: string; - parentName?: string; className?: string; + parentName?: string; } -function Account ({ address, className, isExternal, parentName }: Props): React.ReactElement { +function Account ({ address, className, isExternal, parentName, suri }: Props): React.ReactElement { const onAction = useContext(ActionContext); const [isEditing, setEditing] = useState(false); const [editedName, setName] = useState(null); @@ -27,48 +26,63 @@ function Account ({ address, className, isExternal, parentName }: Props): React. const _saveChanges = useCallback((): void => { if (editedName && editedName !== name) { editAccount(address, editedName) - .then((): void => onAction()) - .catch((error: Error) => console.error(error)); + .then(() => onAction()) + .catch(console.error); } _toggleEdit(); }, [editedName, address, _toggleEdit, onAction]); + const _actions = useMemo(() => ( + <> + + Rename + + {!isExternal && ( + + Derive New Account + + )} +
+ {!isExternal && ( + + Export Account + + )} + + Forget Account + + + ), [_toggleEdit, address, isExternal]); + return (
- - Rename - {!isExternal && ( - Derive New Account - )} - - {!isExternal && ( - - Export Account - - )} - - Forget Account - - - )} + actions={_actions} address={address} + className='address' name={editedName} parentName={parentName} + suri={suri} > {isEditing && ( theme.boxBorderColor}; -`; - -const MenuItem = styled(Link)` - padding: 4px 16px; - display: block; - border-radius: 8px; - font-weight: 600; - font-size: 15px; - line-height: 20px; -`; - -MenuItem.displayName = 'MenuItem'; - -export default styled(Account)` - ${Address} { +export default styled(Account)(({ theme }: ThemeProps) => ` + .address { margin-bottom: 8px; } - .edit-name { + .divider { + padding-top: 16px; + margin-bottom: 16px; + border-bottom: 1px solid ${theme.inputBorderColor}; + } + + .editName { position: absolute; flex: 1; left: 80px; top: 6px; width: 315px; } -`; + + .menuItem { + border-radius: 8px; + display: block; + font-weight: 600; + font-size: 15px; + line-height: 20px; + padding: 4px 16px; + } +`); diff --git a/packages/extension-ui/src/Popup/Accounts/AccountsTree.tsx b/packages/extension-ui/src/Popup/Accounts/AccountsTree.tsx index b6e36312..4dcad477 100644 --- a/packages/extension-ui/src/Popup/Accounts/AccountsTree.tsx +++ b/packages/extension-ui/src/Popup/Accounts/AccountsTree.tsx @@ -10,12 +10,13 @@ interface Props extends AccountWithChildren { parentName?: string; } -function AccountsTree ({ parentName, ...account }: Props): React.ReactElement { +export default function AccountsTree ({ parentName, suri, ...account }: Props): React.ReactElement { return ( <> {account?.children?.map((child, index) => ( ); } - -export default AccountsTree; diff --git a/packages/extension-ui/src/Popup/Derive/index.tsx b/packages/extension-ui/src/Popup/Derive/index.tsx index d5a0dbe4..9fb6b754 100644 --- a/packages/extension-ui/src/Popup/Derive/index.tsx +++ b/packages/extension-ui/src/Popup/Derive/index.tsx @@ -14,28 +14,38 @@ interface Props { isLocked?: boolean; } +interface AddressState { + address: string; +} + +interface PathState extends AddressState { + suri: string; +} + +interface ConfirmState { + account: PathState; + parentPassword: string; +} + function Derive ({ isLocked }: Props): React.ReactElement { const onAction = useContext(ActionContext); - const { address: parentAddress } = useParams<{ address: string }>(); - const [account, setAccount] = useState(null); + const { address: parentAddress } = useParams(); + const [account, setAccount] = useState(null); const [name, setName] = useState(null); const [password, setPassword] = useState(null); const [parentPassword, setParentPassword] = useState(null); - const _onCreate = useCallback(async () => { + const _onCreate = useCallback(() => { if (!account || !name || !password || !parentPassword) { return; } - await deriveAccount(parentAddress, account.suri, parentPassword, name, password); - - onAction('/'); + deriveAccount(parentAddress, account.suri, parentPassword, name, password) + .then(() => onAction('/')) + .catch(console.error); }, [account, name, password, onAction, parentAddress, parentPassword]); - const _onDerivationConfirmed = useCallback(({ account, parentPassword }: { - account: { address: string; suri: string }; - parentPassword: string; - }) => { + const _onDerivationConfirmed = useCallback(({ account, parentPassword }: ConfirmState) => { setAccount(account); setParentPassword(parentPassword); }, []); diff --git a/packages/extension-ui/src/Popup/index.tsx b/packages/extension-ui/src/Popup/index.tsx index 5763b725..793f6587 100644 --- a/packages/extension-ui/src/Popup/index.tsx +++ b/packages/extension-ui/src/Popup/index.tsx @@ -53,8 +53,7 @@ async function requestMediaAccess (cameraOn: boolean): Promise { function initAccountContext (accounts: AccountJson[]): AccountsContext { const hierarchy = buildHierarchy(accounts); - const isNotExternal = (account: AccountJson): boolean => !account.isExternal; - const master = hierarchy.find(isNotExternal); + const master = hierarchy.find((account) => !account.isExternal); return { accounts, diff --git a/packages/extension-ui/src/components/Address.tsx b/packages/extension-ui/src/components/Address.tsx index afbf1afd..2352290d 100644 --- a/packages/extension-ui/src/components/Address.tsx +++ b/packages/extension-ui/src/components/Address.tsx @@ -33,6 +33,7 @@ interface Props { genesisHash?: string | null; actions?: React.ReactNode; parentName?: string | null; + suri?: string; } interface Recoded { @@ -69,7 +70,7 @@ function recodeAddress (address: string, accounts: AccountWithChildren[], chain: const ACCOUNTS_SCREEN_HEIGHT = 500; -function Address ({ actions, address, children, className, genesisHash, name, parentName }: Props): React.ReactElement { +function Address ({ actions, address, children, className, genesisHash, name, parentName, suri }: Props): React.ReactElement { const { accounts } = useContext(AccountContext); const chain = useMetadata(genesisHash); const [{ account, formatted, prefix }, setRecoded] = useState({ account: null, formatted: null, prefix: 42 }); @@ -119,7 +120,7 @@ function Address ({ actions, address, children, className, genesisHash, name, pa <> - {parentName} + {parentName}{suri || ''} {displayedName} diff --git a/packages/extension-ui/src/components/Link.tsx b/packages/extension-ui/src/components/Link.tsx index 2a7502f0..97ac2132 100644 --- a/packages/extension-ui/src/components/Link.tsx +++ b/packages/extension-ui/src/components/Link.tsx @@ -38,20 +38,20 @@ function Link ({ children, className, onClick, to }: Props): React.ReactElement< ); } -export default styled(Link)` - display: flex; +export default styled(Link)(({ isDanger, theme }: Props) => ` align-items: center; - color: ${({ isDanger, theme }: Props): string => isDanger ? theme.textColorDanger : theme.textColor}; + color: ${isDanger ? theme.textColorDanger : theme.textColor}; + display: flex; opacity: 0.9; text-decoration: none; vertical-align: middle; &:hover { - color: ${({ isDanger, theme }: Props): string => isDanger ? theme.textColorDanger : theme.textColor}; + color: ${isDanger ? theme.textColorDanger : theme.textColor}; opacity: 1.0; } &:visited { - color: ${({ isDanger, theme }: Props): string => isDanger ? theme.textColorDanger : theme.textColor}; + color: ${isDanger ? theme.textColorDanger : theme.textColor}; } -`; +`); diff --git a/packages/extension-ui/src/utils/buildHierarchy.ts b/packages/extension-ui/src/utils/buildHierarchy.ts index 193f37d2..285f737d 100644 --- a/packages/extension-ui/src/utils/buildHierarchy.ts +++ b/packages/extension-ui/src/utils/buildHierarchy.ts @@ -4,28 +4,33 @@ import { AccountJson, AccountWithChildren } from '@polkadot/extension-base/background/types'; -const isRoot = (account: AccountJson): boolean => !account.parentAddress; -const isChild = (parent: AccountJson) => (account: AccountJson): boolean => account.parentAddress === parent.address; -const compareByCreationTime = (a: AccountJson, b: AccountJson): number => (a.whenCreated || Infinity) - (b.whenCreated || Infinity); +type ChildFilter = (account: AccountJson) => AccountWithChildren; -export const accountWithChildren = (allAccounts: AccountJson[]) => (account: AccountJson): AccountWithChildren => { - const children = allAccounts - .filter(isChild(account)) - .map(accountWithChildren(allAccounts)) - .sort(compareByCreationTime); +function compareByCreationTime (a: AccountJson, b: AccountJson): number { + return (a.whenCreated || Infinity) - (b.whenCreated || Infinity); +} - if (children.length === 0) { - return account; - } +export function accountWithChildren (accounts: AccountJson[]): ChildFilter { + return (account: AccountJson): AccountWithChildren => { + const children = accounts + .filter(({ parentAddress }) => account.address === parentAddress) + .map(accountWithChildren(accounts)) + .sort(compareByCreationTime); - return { - ...account, - children + return children.length === 0 + ? account + : { children, ...account }; }; -}; +} -export const buildHierarchy = (accounts: AccountJson[]): AccountWithChildren[] => - accounts - .filter(isRoot) +export function buildHierarchy (accounts: AccountJson[]): AccountWithChildren[] { + return accounts + .filter(({ parentAddress }) => + // it is a parent + !parentAddress || + // we don't have a parent for this one + !accounts.some(({ address }) => parentAddress === address) + ) .map(accountWithChildren(accounts)) .sort(compareByCreationTime); +}