Display accounts first-and-foremost on all creations (#426)

* Display accounts first-and-foremost on all creations

* Fix tests
This commit is contained in:
Jaco Greeff 2020-09-03 22:36:49 +02:00 committed by GitHub
parent 04d0b2700f
commit ae5d076de9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 145 additions and 97 deletions

View File

@ -14,6 +14,7 @@ Changes:
- Derived accounts uses the parent genesisHash by default (attaches to same chain) - Derived accounts uses the parent genesisHash by default (attaches to same chain)
- Make import from seed, QR & JSON options available on first-start - Make import from seed, QR & JSON options available on first-start
- Adjust popup width, allowing full display of e.g. addresses - Adjust popup width, allowing full display of e.g. addresses
- Always display network selection on all accounts
- Handling signing rejections (any order) transparently - Handling signing rejections (any order) transparently
- Small overall UI and use adjustments - Small overall UI and use adjustments
- Latest upstream polkadot-js dependencies - Latest upstream polkadot-js dependencies

View File

@ -65,7 +65,7 @@ const WINDOW_OPTS = {
top: 150, top: 150,
type: 'popup', type: 'popup',
url: chrome.extension.getURL('index.html'), url: chrome.extension.getURL('index.html'),
width: 480 width: 560
}; };
function getId (): string { function getId (): string {

View File

@ -5,7 +5,7 @@
import { RequestAuthorizeTab } from '@polkadot/extension-base/background/types'; import { RequestAuthorizeTab } from '@polkadot/extension-base/background/types';
import { ThemeProps } from '../../types'; import { ThemeProps } from '../../types';
import React, { useContext } from 'react'; import React, { useCallback, useContext } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { ActionBar, ActionContext, Button, Icon, Link, Warning } from '../../components'; import { ActionBar, ActionContext, Button, Icon, Link, Warning } from '../../components';
@ -21,14 +21,20 @@ interface Props {
function Request ({ authId, className, isFirst, request: { origin }, url }: Props): React.ReactElement<Props> { function Request ({ authId, className, isFirst, request: { origin }, url }: Props): React.ReactElement<Props> {
const onAction = useContext(ActionContext); const onAction = useContext(ActionContext);
const _onApprove = (): Promise<void> =>
approveAuthRequest(authId) const _onApprove = useCallback(
() => approveAuthRequest(authId)
.then(() => onAction()) .then(() => onAction())
.catch((error: Error) => console.error(error)); .catch((error: Error) => console.error(error)),
const _onReject = (): Promise<void> => [authId, onAction]
rejectAuthRequest(authId) );
const _onReject = useCallback(
() => rejectAuthRequest(authId)
.then(() => onAction()) .then(() => onAction())
.catch((error: Error) => console.error(error)); .catch((error: Error) => console.error(error)),
[authId, onAction]
);
return ( return (
<div className={className}> <div className={className}>

View File

@ -4,7 +4,7 @@
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { Address, BackButton, ButtonArea, NextStepButton, VerticalSpace } from '../../components'; import { BackButton, ButtonArea, NextStepButton, VerticalSpace } from '../../components';
import { Name, Password } from '../../partials'; import { Name, Password } from '../../partials';
interface Props { interface Props {
@ -14,7 +14,7 @@ interface Props {
onCreate: (name: string, password: string) => void | Promise<void | boolean>; onCreate: (name: string, password: string) => void | Promise<void | boolean>;
} }
function AccountName ({ address, isBusy, onBackClick, onCreate }: Props): React.ReactElement<Props> { function AccountName ({ isBusy, onBackClick, onCreate }: Props): React.ReactElement<Props> {
const [name, setName] = useState<string | null>(null); const [name, setName] = useState<string | null>(null);
const [password, setPassword] = useState<string | null>(null); const [password, setPassword] = useState<string | null>(null);
@ -29,13 +29,7 @@ function AccountName ({ address, isBusy, onBackClick, onCreate }: Props): React.
isFocused isFocused
onChange={setName} onChange={setName}
/> />
{name && <Password onChange={setPassword} />} <Password onChange={setPassword} />
{name && password && (
<Address
address={address}
name={name}
/>
)}
<VerticalSpace /> <VerticalSpace />
<ButtonArea> <ButtonArea>
<BackButton onClick={onBackClick} /> <BackButton onClick={onBackClick} />
@ -52,4 +46,4 @@ function AccountName ({ address, isBusy, onBackClick, onCreate }: Props): React.
); );
} }
export default AccountName; export default React.memo(AccountName);

View File

@ -42,7 +42,7 @@ describe('Create Account', () => {
wrapper.update(); wrapper.update();
}; };
const enterName = (name: string): Promise<void> => type(wrapper.find('input'), name); 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 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); const repeat = (password: string) => (): Promise<void> => type(wrapper.find('input[type="password"]').last(), password);
@ -89,20 +89,20 @@ describe('Create Account', () => {
it('only account name input is visible at first', () => { 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-name]').find(Input)).toHaveLength(1);
expect(wrapper.find(InputWithLabel).find('[data-input-password]')).toHaveLength(0); 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(InputWithLabel).find('[data-input-repeat-password]')).toHaveLength(0);
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(true); 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', () => { it('input should not be highlighted as error until first interaction', () => {
expect(wrapper.find(Input).prop('withError')).toBe(false); 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 () => { it('after typing less than 3 characters into name input, password input is not visible', async () => {
await enterName('ab'); await enterName('ab');
expect(wrapper.find(Input).prop('withError')).toBe(true); expect(wrapper.find(InputWithLabel).find('[data-input-name]').find(Input).prop('withError')).toBe(true);
expect(wrapper.find('ErrorMessage').text()).toBe('Account name is too short'); expect(wrapper.find('ErrorMessage').first().text()).toBe('Account name is too short');
expect(wrapper.find(InputWithLabel).find('[data-input-password]')).toHaveLength(0); 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(InputWithLabel).find('[data-input-repeat-password]')).toHaveLength(0);
expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(true); expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(true);
}); });
@ -110,12 +110,12 @@ describe('Create Account', () => {
it('input should keep showing error when something has been typed but then erased', async () => { it('input should keep showing error when something has been typed but then erased', async () => {
await enterName('ab'); await enterName('ab');
await enterName(''); await enterName('');
expect(wrapper.find(Input).prop('withError')).toBe(true); expect(wrapper.find(InputWithLabel).find('[data-input-name]').find(Input).prop('withError')).toBe(true);
}); });
it('after typing 3 characters into name input, first password input is visible', async () => { it('after typing 3 characters into name input, first password input is visible', async () => {
await enterName('abc'); await enterName('abc');
expect(wrapper.find(Input).first().prop('withError')).toBe(false); 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-password]').find(Input)).toHaveLength(1);
expect(wrapper.find(InputWithLabel).find('[data-input-repeat-password]')).toHaveLength(0); 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); expect(wrapper.find('[data-button-action="add new root"] button').prop('disabled')).toBe(true);

View File

@ -33,12 +33,11 @@ function Mnemonic ({ onNextStep, seed }: Props): React.ReactElement<Props> {
return ( return (
<> <>
<Warning>Please write down your wallet’s mnemonic seed and keep it in a safe place. <br />
Mnemonic seed is used to restore your wallet. Keep it carefully in case you lose your assets.</Warning>
<MnemonicSeed <MnemonicSeed
onCopy={_onCopy} onCopy={_onCopy}
seed={seed} seed={seed}
/> />
<Warning>Please write down your wallet’s mnemonic seed and keep it in a safe place. The mnemonic can be used to restore your wallet. Keep it carefully to not lose your assets.</Warning>
<VerticalSpace /> <VerticalSpace />
<Checkbox <Checkbox
checked={isMnemonicSaved} checked={isMnemonicSaved}
@ -57,4 +56,4 @@ function Mnemonic ({ onNextStep, seed }: Props): React.ReactElement<Props> {
); );
} }
export default Mnemonic; export default React.memo(Mnemonic);

View File

@ -3,7 +3,7 @@
// of the Apache-2.0 license. See the LICENSE file for details. // of the Apache-2.0 license. See the LICENSE file for details.
import React, { useCallback, useContext, useEffect, useState } from 'react'; import React, { useCallback, useContext, useEffect, useState } from 'react';
import { ActionContext, Loading } from '../../components'; import { ActionContext, Address, Loading } from '../../components';
import { createAccountSuri, createSeed } from '../../messaging'; import { createAccountSuri, createSeed } from '../../messaging';
import { HeaderWithSteps } from '../../partials'; import { HeaderWithSteps } from '../../partials';
import AccountName from './AccountName'; import AccountName from './AccountName';
@ -27,6 +27,7 @@ export default function CreateAccount (): React.ReactElement {
// this should always be the case // this should always be the case
if (name && password && account) { if (name && password && account) {
setIsBusy(true); setIsBusy(true);
createAccountSuri(name, password, account.seed) createAccountSuri(name, password, account.seed)
.then(() => onAction('/')) .then(() => onAction('/'))
.catch((error: Error): void => { .catch((error: Error): void => {
@ -47,23 +48,31 @@ export default function CreateAccount (): React.ReactElement {
step={step} step={step}
text='Create an account:&nbsp;' text='Create an account:&nbsp;'
/> />
<Loading>{account && ( <Loading>
step === 1 <div>
? ( <Address
<Mnemonic address={account?.address}
onNextStep={_onNextStep} name={name}
seed={account.seed} />
/> </div>
) {account && (
: ( step === 1
<AccountName ? (
address={account.address} <Mnemonic
isBusy={isBusy} onNextStep={_onNextStep}
onBackClick={_onPreviousStep} seed={account.seed}
onCreate={_onCreate} />
/> )
) : (
)}</Loading> <AccountName
address={account.address}
isBusy={isBusy}
onBackClick={_onPreviousStep}
onCreate={_onCreate}
/>
)
)}
</Loading>
</> </>
); );
} }

View File

@ -12,13 +12,14 @@ import { Address, Svg } from '../../components';
import useOutsideClick from '../../hooks/useOutsideClick'; import useOutsideClick from '../../hooks/useOutsideClick';
interface Props { interface Props {
allAddresses: string[]; allAddresses: [string, string | null][];
className?: string; className?: string;
onSelect: (address: string) => void; onSelect: (address: string) => void;
selectedAddress: string; selectedAddress: string;
selectedGenesis: string | null;
} }
function AddressDropdown ({ allAddresses, className, onSelect, selectedAddress }: Props): React.ReactElement<Props> { function AddressDropdown ({ allAddresses, className, onSelect, selectedAddress, selectedGenesis }: Props): React.ReactElement<Props> {
const [isDropdownVisible, setDropdownVisible] = useState(false); const [isDropdownVisible, setDropdownVisible] = useState(false);
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
@ -34,16 +35,22 @@ function AddressDropdown ({ allAddresses, className, onSelect, selectedAddress }
onClick={_toggleDropdown} onClick={_toggleDropdown}
ref={ref} ref={ref}
> >
<Address address={selectedAddress} /> <Address
address={selectedAddress}
genesisHash={selectedGenesis}
/>
</div> </div>
<HiddenOptions visible={isDropdownVisible}> <HiddenOptions visible={isDropdownVisible}>
{allAddresses.map((address) => ( {allAddresses.map(([address, genesisHash]) => (
<div <div
data-parent-option data-parent-option
key={address} key={address}
onClick={_selectParent(address)} onClick={_selectParent(address)}
> >
<Address address={address} /> <Address
address={address}
genesisHash={genesisHash}
/>
</div> </div>
))} ))}
</HiddenOptions> </HiddenOptions>
@ -54,7 +61,7 @@ function AddressDropdown ({ allAddresses, className, onSelect, selectedAddress }
const HiddenOptions = styled.div<{ visible: boolean }>` const HiddenOptions = styled.div<{ visible: boolean }>`
position: absolute; position: absolute;
visibility: ${({ visible }): string => (visible ? 'visible' : 'hidden')}; visibility: ${({ visible }): string => (visible ? 'visible' : 'hidden')};
width: 430px; width: 510px;
z-index: 100; z-index: 100;
background: ${({ theme }: ThemeProps): string => theme.bodyColor}; background: ${({ theme }: ThemeProps): string => theme.bodyColor};
max-height: ${({ visible }): string => (visible ? '200px' : '0')}; max-height: ${({ visible }): string => (visible ? '200px' : '0')};
@ -77,7 +84,7 @@ export default styled(AddressDropdown)(({ theme }: ThemeProps) => `
& > div:first-child > ${Address}::after { & > div:first-child > ${Address}::after {
content: ''; content: '';
position: absolute; position: absolute;
top: 50%; top: 66%;
transform: translateY(-50%); transform: translateY(-50%);
right: 11px; right: 11px;
width: 30px; width: 30px;

View File

@ -2,7 +2,7 @@
// This software may be modified and distributed under the terms // This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details. // of the Apache-2.0 license. See the LICENSE file for details.
import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'; import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { assert } from '@polkadot/util'; import { assert } from '@polkadot/util';
@ -15,10 +15,11 @@ import DerivationPath from './DerivationPath';
interface Props { interface Props {
isLocked?: boolean; isLocked?: boolean;
parentAddress: string; parentAddress: string;
parentGenesis: string | null;
onDerivationConfirmed: (derivation: { account: { address: string; suri: string }; parentPassword: string }) => void; onDerivationConfirmed: (derivation: { account: { address: string; suri: string }; parentPassword: string }) => void;
} }
export function SelectParent ({ isLocked, onDerivationConfirmed, parentAddress }: Props): React.ReactElement<Props> { export function SelectParent ({ isLocked, onDerivationConfirmed, parentAddress, parentGenesis }: Props): React.ReactElement<Props> {
const onAction = useContext(ActionContext); const onAction = useContext(ActionContext);
const [isBusy, setIsBusy] = useState(false); const [isBusy, setIsBusy] = useState(false);
const { accounts, hierarchy } = useContext(AccountContext); const { accounts, hierarchy } = useContext(AccountContext);
@ -30,6 +31,13 @@ export function SelectParent ({ isLocked, onDerivationConfirmed, parentAddress }
const passwordInputRef = useRef<HTMLDivElement>(null); const passwordInputRef = useRef<HTMLDivElement>(null);
const allAddresses = useMemo(
() => hierarchy
.filter(({ isExternal }) => !isExternal)
.map(({ address, genesisHash }): [string, string | null] => [address, genesisHash || null]),
[hierarchy]
);
const _goCreate = useCallback( const _goCreate = useCallback(
() => onAction('/account/create'), () => onAction('/account/create'),
[onAction] [onAction]
@ -91,13 +99,19 @@ export function SelectParent ({ isLocked, onDerivationConfirmed, parentAddress }
)} )}
<DisableableArea isDisabled={!shouldAccountBeDerived}> <DisableableArea isDisabled={!shouldAccountBeDerived}>
{isLocked {isLocked
? <Address address={parentAddress} /> ? (
<Address
address={parentAddress}
genesisHash={parentGenesis}
/>
)
: ( : (
<Label label='Choose Parent Account:'> <Label label='Choose Parent Account:'>
<AddressDropdown <AddressDropdown
allAddresses={hierarchy.filter(({ isExternal }) => !isExternal).map(({ address }) => address)} allAddresses={allAddresses}
onSelect={_onParentChange} onSelect={_onParentChange}
selectedAddress={parentAddress} selectedAddress={parentAddress}
selectedGenesis={parentGenesis}
/> />
</Label> </Label>
) )

View File

@ -2,7 +2,7 @@
// This software may be modified and distributed under the terms // This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details. // of the Apache-2.0 license. See the LICENSE file for details.
import React, { useCallback, useContext, useState } from 'react'; import React, { useCallback, useContext, useMemo, useState } from 'react';
import { useParams } from 'react-router'; import { useParams } from 'react-router';
import { AccountContext, ActionContext, Address, BackButton, ButtonArea, NextStepButton, VerticalSpace } from '../../components'; import { AccountContext, ActionContext, Address, BackButton, ButtonArea, NextStepButton, VerticalSpace } from '../../components';
@ -37,22 +37,24 @@ function Derive ({ isLocked }: Props): React.ReactElement<Props> {
const [password, setPassword] = useState<string | null>(null); const [password, setPassword] = useState<string | null>(null);
const [parentPassword, setParentPassword] = useState<string | null>(null); const [parentPassword, setParentPassword] = useState<string | null>(null);
const parentGenesis = useMemo(
() => accounts.find((a) => a.address === parentAddress)?.genesisHash || null,
[accounts, parentAddress]
);
const _onCreate = useCallback(() => { const _onCreate = useCallback(() => {
if (!account || !name || !password || !parentPassword) { if (!account || !name || !password || !parentPassword) {
return; return;
} }
const parentAccount = accounts.find((a) => a.address === parentAddress);
const genesisHash = parentAccount?.genesisHash || null;
setIsBusy(true); setIsBusy(true);
deriveAccount(parentAddress, account.suri, parentPassword, name, password, genesisHash) deriveAccount(parentAddress, account.suri, parentPassword, name, password, parentGenesis)
.then(() => onAction('/')) .then(() => onAction('/'))
.catch((error): void => { .catch((error): void => {
setIsBusy(false); setIsBusy(false);
console.error(error); console.error(error);
}); });
}, [account, accounts, name, password, onAction, parentAddress, parentPassword]); }, [account, name, password, onAction, parentAddress, parentGenesis, parentPassword]);
const _onDerivationConfirmed = useCallback(({ account, parentPassword }: ConfirmState) => { const _onDerivationConfirmed = useCallback(({ account, parentPassword }: ConfirmState) => {
setAccount(account); setAccount(account);
@ -74,23 +76,23 @@ function Derive ({ isLocked }: Props): React.ReactElement<Props> {
isLocked={isLocked} isLocked={isLocked}
onDerivationConfirmed={_onDerivationConfirmed} onDerivationConfirmed={_onDerivationConfirmed}
parentAddress={parentAddress} parentAddress={parentAddress}
/> parentGenesis={parentGenesis}
)}
{account && (
<Name
isFocused
onChange={setName}
/>
)}
{account && name && <Password onChange={setPassword}/>}
{account && name && password && (
<Address
address={account.address}
name={name}
/> />
)} )}
{account && ( {account && (
<> <>
<div>
<Address
address={account.address}
genesisHash={parentGenesis}
name={name}
/>
</div>
<Name
isFocused
onChange={setName}
/>
<Password onChange={setPassword} />
<VerticalSpace/> <VerticalSpace/>
<ButtonArea> <ButtonArea>
<BackButton onClick={_onBackClick}/> <BackButton onClick={_onBackClick}/>
@ -108,4 +110,4 @@ function Derive ({ isLocked }: Props): React.ReactElement<Props> {
); );
} }
export default Derive; export default React.memo(Derive);

View File

@ -36,14 +36,16 @@ export default function ImportQr (): React.ReactElement {
)} )}
{account && ( {account && (
<> <>
<div>
<Address
{...account}
name={name}
/>
</div>
<Name <Name
isFocused isFocused
onChange={setName} onChange={setName}
/> />
<Address
{...account}
name={name}
/>
<VerticalSpace /> <VerticalSpace />
{name && ( {name && (
<ButtonArea> <ButtonArea>

View File

@ -40,10 +40,14 @@ export default function Import (): React.ReactElement {
const _onChangeSeed = useCallback( const _onChangeSeed = useCallback(
async (suri: string | null): Promise<void> => { async (suri: string | null): Promise<void> => {
if (suri) { if (suri) {
setAccount(await validateSeed(suri)); try {
} else { return setAccount(await validateSeed(suri));
setAccount(null); } catch (error) {
console.error(error);
}
} }
setAccount(null);
}, },
[] []
); );
@ -69,6 +73,12 @@ export default function Import (): React.ReactElement {
showBackArrow showBackArrow
text='Import account' text='Import account'
/> />
<div>
<Address
address={account?.address}
name={name}
/>
</div>
<ValidatedInput <ValidatedInput
// eslint-disable-next-line @typescript-eslint/no-use-before-define // eslint-disable-next-line @typescript-eslint/no-use-before-define
component={SeedInput} component={SeedInput}
@ -78,14 +88,14 @@ export default function Import (): React.ReactElement {
rowsCount={2} rowsCount={2}
validator={isSeedValid} validator={isSeedValid}
/> />
{account && <Name onChange={setName} />} {account && (
{account && name && <Password onChange={setPassword} />} <>
<Name onChange={setName} />
<Password onChange={setPassword} />
</>
)}
{account && name && password && ( {account && name && password && (
<> <>
<Address
address={account.address}
name={name}
/>
<VerticalSpace /> <VerticalSpace />
<ButtonArea> <ButtonArea>
<NextStepButton <NextStepButton

View File

@ -96,10 +96,13 @@ export default function Upload (): React.ReactElement {
text='Restore from JSON' text='Restore from JSON'
/> />
<div> <div>
<Address <div>
address={(isFileValid && address) || null} <Address
name={(isFileValid && json?.meta.name as string) || null} address={(isFileValid && address) || null}
/> genesisHash={(isFileValid && json?.meta.genesisHash as string) || null}
name={(isFileValid && json?.meta.name as string) || null}
/>
</div>
<InputFileWithLabel <InputFileWithLabel
accept={acceptedFormats} accept={acceptedFormats}
isError={!isFileValid} isError={!isFileValid}

View File

@ -48,6 +48,7 @@ export default function Request ({ account: { isExternal }, buttonText, isFirst,
useEffect((): void => { useEffect((): void => {
setIsLocked(null); setIsLocked(null);
!isExternal && isSignLocked(signId) !isExternal && isSignLocked(signId)
.then((isLocked) => setIsLocked(isLocked)) .then((isLocked) => setIsLocked(isLocked))
.catch((error: Error) => console.error(error)); .catch((error: Error) => console.error(error));

View File

@ -40,7 +40,7 @@ function MnemonicSeed ({ className, onCopy, seed }: Props): React.ReactElement<P
} }
export default styled(MnemonicSeed)(({ theme }: ThemeProps) => ` export default styled(MnemonicSeed)(({ theme }: ThemeProps) => `
margin-top: 21px; margin-bottom: 21px;
.buttonsRow { .buttonsRow {
display: flex; display: flex;

View File

@ -5,7 +5,7 @@
<title>polkadot{.js}</title> <title>polkadot{.js}</title>
</head> </head>
<body style="height: 600px; margin:0; overflow-x:hidden; text-align:center"> <body style="height: 600px; margin:0; overflow-x:hidden; text-align:center">
<div id="root" style="box-sizing:border-box; margin:0 auto; padding:0; text-align:left; width:570px; height: calc(100% - 2px); max-width: 100%;"></div> <div id="root" style="box-sizing:border-box; margin:0 auto; padding:0; text-align:left; width:560px; height: calc(100% - 2px); max-width: 100%;"></div>
<script src='./extension.js'></script> <script src='./extension.js'></script>
</body> </body>
</html> </html>