Skip to content

Commit

Permalink
Introduce the enable() interface (prompts user) (#20)
Browse files Browse the repository at this point in the history
* Introduce the enable() interface

* Provider

* Add auth requests to state

* injector comments

* Background extension list, spprove & reject

* Cleanups

* Untested, but the UI parts are there (test next)

* linting

* Not pretty, but seems to auth

* Small styling adjustments

* Adjust Box layout slightly

* Small styling adjustments

* signing -> transactions

* isDanger for links

* Styling defaults
  • Loading branch information
jacogr authored May 23, 2019
1 parent 4a2768c commit f241e71
Show file tree
Hide file tree
Showing 34 changed files with 582 additions and 247 deletions.
44 changes: 30 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,29 +24,45 @@ Once added, you can create an account (via a generated seed) or import via an ex
The extension injects `injectedWeb3` into the global `window` object, exposing the following:

```js
// a version that identifies the actual injection version (future-use)
type Version = 0;
window.injectedWeb3 = {
// this is the name for this extension, there could be multiples injected,
// each with their own keys
'polkadot-js': {
// same as above, just easier to manage having accessible here
name: 'polkadot-js',
// semver for the package
version: '0.1.0',

// this is called to enable the injection, and returns an injected
// object containing the accounts, signer and provider interfaces
// (or it will reject if not authorized)
enable (originName: string): Promise<Injected>
}
}
```

When there is more than one extension, each will populate an entry above, so from an extension implementation perspective, the structure should not be overridded. The injected interface, when returned via `enable`, contains the following -

// an interface describing an account
interface Account {
readonly address: string; // ss-58 encoded address
readonly name?: string; // optional name for display
```js
interface Injected {
readonly accounts: Accounts;
readonly signer: Signer;
// not injected as of yet, subscriptable provider for polkadot-js injection
// readonly provider: Provider
}

// exposes accounts
interface Accounts {
get: () => Promise<Array<Account>>;
get (): Promise<Array<{
readonly address: string; // ss-58 encoded address
readonly name?: string; // optional name for display
}>>;
}

// a signer that communicates with the extension via sendMessage
interface Signer extends SignerInterface {
// no specific signer extensions
}

interface Injected {
readonly accounts: Accounts;
readonly signer: Signer;
readonly version: Version;
// no specific signer extensions, exposes the `sign` interface for use by
// the polkadot-js API
}
```

Expand Down
7 changes: 3 additions & 4 deletions packages/extension-ui/src/Popup/Accounts/Account.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
import { OnActionFromCtx } from '../../components/types';

import React, { useState } from 'react';
import { Link } from 'react-router-dom';

import { ActionBar, Address, withAction } from '../../components';
import { ActionBar, Address, Link, withOnAction } from '../../components';
import { editAccount } from '../../messaging';
import { Name } from '../../partials';

Expand Down Expand Up @@ -53,11 +52,11 @@ function Account ({ address, className, onAction }: Props) {
}
>
<ActionBar>
<a href='#' onClick={toggleEdit}>Edit</a>
<Link onClick={toggleEdit}>Edit</Link>
<Link to={`/account/forget/${address}`}>Forget</Link>
</ActionBar>
</Address>
);
}

export default withAction(Account);
export default withOnAction(Account);
56 changes: 56 additions & 0 deletions packages/extension-ui/src/Popup/Authorize/Request.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.

import { MessageAuthorize } from '@polkadot/extension/background/types';
import { OnActionFromCtx } from '../../components/types';

import React from 'react';
import styled from 'styled-components';

import { ActionBar, Box, Button, Link, defaults, withOnAction } from '../../components';
import { approveAuthRequest, rejectAuthRequest } from '../../messaging';

type Props = {
authId: number,
className?: string,
isFirst: boolean,
onAction: OnActionFromCtx,
request: MessageAuthorize,
url: string
};

function Request ({ authId, className, isFirst, onAction, request: { origin }, url }: Props) {
const onApprove = (): Promise<void> =>
approveAuthRequest(authId)
.then(() => onAction())
.catch(console.error);
const onReject = (): void => {
rejectAuthRequest(authId)
.then(() => onAction())
.catch(console.error);
};

return (
<Box className={className}>
<div>The application, identified as <div className='tab-name'>{origin}</div> is requesting access from <div className='tab-url'>{url}</div> to the accounts and signing capabilities of this extension. Only approve the request if you trust the application.</div>
<ActionBar>
<Link isDanger onClick={onReject}>Reject</Link>
</ActionBar>
{isFirst && (
<Button
label='Yes, allow this application access'
onClick={onApprove}
/>
)}
</Box>
);
}

export default withOnAction(styled(Request)`
.tab-name,
.tab-url {
color: ${defaults.linkColor};
display: inline-block;
}
`);
33 changes: 33 additions & 0 deletions packages/extension-ui/src/Popup/Authorize/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright 2019 @polkadot/extension-ui authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.

import { AuthRequestsFromCtx } from '../../components/types';

import React from 'react';

import { Header, withAuthRequests } from '../../components';
import Request from './Request';

type Props = {
requests: AuthRequestsFromCtx
};

function Authorize ({ requests }: Props) {
return (
<div>
<Header label='authorize' />
{requests.map(([id, request, url], index) => (
<Request
authId={id}
isFirst={index === 0}
key={id}
request={request}
url={url}
/>
))}
</div>
);
}

export default withAuthRequests(Authorize);
4 changes: 2 additions & 2 deletions packages/extension-ui/src/Popup/Create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { OnActionFromCtx } from '../components/types';

import React, { useState, useEffect } from 'react';

import { Address, Button, Header, Loading, TextArea, withAction } from '../components';
import { Address, Button, Header, Loading, TextArea, withOnAction } from '../components';
import { createAccount, createSeed } from '../messaging';
import { Back, Name, Password } from '../partials';

Expand Down Expand Up @@ -69,4 +69,4 @@ function Create ({ onAction }: Props) {
);
}

export default withAction(Create);
export default withOnAction(Create);
4 changes: 2 additions & 2 deletions packages/extension-ui/src/Popup/Forget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { OnActionFromCtx } from '../components/types';
import React from 'react';
import { RouteComponentProps, withRouter } from 'react-router';

import { Address, Button, Header, Tip, withAction } from '../components';
import { Address, Button, Header, Tip, withOnAction } from '../components';
import { forgetAccount } from '../messaging';
import { Back } from '../partials';

Expand Down Expand Up @@ -37,4 +37,4 @@ function Forget ({ match: { params: { address } }, onAction }: Props) {
);
}

export default withAction(withRouter(Forget));
export default withOnAction(withRouter(Forget));
4 changes: 2 additions & 2 deletions packages/extension-ui/src/Popup/Import.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { OnActionFromCtx } from '../components/types';

import React, { useState } from 'react';

import { Address, Button, Header, TextArea, withAction } from '../components';
import { Address, Button, Header, TextArea, withOnAction } from '../components';
import { createAccount, validateSeed } from '../messaging';
import { Back, Name, Password } from '../partials';

Expand Down Expand Up @@ -67,4 +67,4 @@ function Import ({ onAction }: Props) {
);
}

export default withAction(Import);
export default withOnAction(Import);
12 changes: 6 additions & 6 deletions packages/extension-ui/src/Popup/Signing/Request.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { OnActionFromCtx } from '../../components/types';

import React from 'react';

import { ActionBar, Address, withAction } from '../../components';
import { approveRequest, cancelRequest } from '../../messaging';
import { ActionBar, Address, Link, withOnAction } from '../../components';
import { approveSignRequest, cancelSignRequest } from '../../messaging';
import Details from './Details';
import Unlock from './Unlock';

Expand All @@ -22,12 +22,12 @@ type Props = {

function Request ({ isFirst, onAction, request: { address, genesisHash, method, nonce }, signId, url }: Props) {
const onCancel = (): void => {
cancelRequest(signId)
cancelSignRequest(signId)
.then(() => onAction())
.catch(console.error);
};
const onSign = (password: string): Promise<void> =>
approveRequest(signId, password).then(() => onAction());
approveSignRequest(signId, password).then(() => onAction());

return (
<Address address={address}>
Expand All @@ -38,11 +38,11 @@ function Request ({ isFirst, onAction, request: { address, genesisHash, method,
url={url}
/>
<ActionBar>
<a href='#' onClick={onCancel}>Cancel</a>
<Link isDanger onClick={onCancel}>Cancel</Link>
</ActionBar>
{isFirst && <Unlock onSign={onSign} />}
</Address>
);
}

export default withAction(Request);
export default withOnAction(Request);
1 change: 1 addition & 0 deletions packages/extension-ui/src/Popup/Signing/Unlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,5 @@ function Unlock ({ className, onSign }: Props) {

export default styled(Unlock)`
margin-bottom: -0.75rem;
margin-left: -3rem;
`;
10 changes: 5 additions & 5 deletions packages/extension-ui/src/Popup/Signing/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,21 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.

import { RequestsFromCtx } from '../../components/types';
import { SignRequestsFromCtx } from '../../components/types';

import React from 'react';

import { Header, withRequests } from '../../components';
import { Header, withSignRequests } from '../../components';
import Request from './Request';

type Props = {
requests: RequestsFromCtx
requests: SignRequestsFromCtx
};

function Signing ({ requests }: Props) {
return (
<div>
<Header label='requests' />
<Header label='transactions' />
{requests.map(([id, request, url], index) => (
<Request
isFirst={index === 0}
Expand All @@ -30,4 +30,4 @@ function Signing ({ requests }: Props) {
);
}

export default withRequests(Signing);
export default withSignRequests(Signing);
43 changes: 27 additions & 16 deletions packages/extension-ui/src/Popup/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,17 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.

import { MessageExtrinsicSign } from '@polkadot/extension/background/types';
import { AuthorizeRequest, SigningRequest } from '@polkadot/extension/background/types';
import { KeyringJson } from '@polkadot/ui-keyring/types';

import React, { useEffect, useState } from 'react';
import { Route, Switch } from 'react-router';

import { Loading } from '../components';
import { AccountContext, ActionContext, SigningContext } from '../components/contexts';
import { getAccounts, getRequests } from '../messaging';
import { AccountContext, ActionContext, AuthorizeContext, SigningContext } from '../components/contexts';
import { getAccounts, getAuthRequests, getSignRequests } from '../messaging';
import Accounts from './Accounts';
import Authorize from './Authorize';
import Create from './Create';
import Forget from './Forget';
import Import from './Import';
Expand All @@ -21,15 +22,17 @@ type Props = {};

export default function Popup (props: Props) {
const [accounts, setAccounts] = useState(null as null | Array<KeyringJson>);
const [requests, setRequests] = useState(null as null | Array<[number, MessageExtrinsicSign, string]>);
const [authRequests, setAuthRequests] = useState(null as null | Array<AuthorizeRequest>);
const [signRequests, setSignRequests] = useState(null as null | Array<SigningRequest>);

const onAction = (to?: string): void => {
// loads all accounts & requests (this is passed through to children to trigger changes)
Promise
.all([getAccounts(), getRequests()])
.then(([accounts, requests]) => {
.all([getAccounts(), getAuthRequests(), getSignRequests()])
.then(([accounts, authRequests, signRequests]) => {
setAccounts(accounts);
setRequests(requests);
setAuthRequests(authRequests);
setSignRequests(signRequests);
})
.catch(console.error);

Expand All @@ -43,18 +46,26 @@ export default function Popup (props: Props) {
onAction();
}, []);

const Root = authRequests && authRequests.length
? Authorize
: signRequests && signRequests.length
? Signing
: Accounts;

return (
<Loading>{accounts && requests && (
<Loading>{accounts && authRequests && signRequests && (
<ActionContext.Provider value={onAction}>
<AccountContext.Provider value={accounts}>
<SigningContext.Provider value={requests}>
<Switch>
<Route path='/account/create' component={Create} />
<Route path='/account/forget/:address' component={Forget} />
<Route path='/account/import' component={Import} />
<Route exact path='/' component={requests.length ? Signing : Accounts} />
</Switch>
</SigningContext.Provider>
<AuthorizeContext.Provider value={authRequests}>
<SigningContext.Provider value={signRequests}>
<Switch>
<Route path='/account/create' component={Create} />
<Route path='/account/forget/:address' component={Forget} />
<Route path='/account/import' component={Import} />
<Route exact path='/' component={Root} />
</Switch>
</SigningContext.Provider>
</AuthorizeContext.Provider>
</AccountContext.Provider>
</ActionContext.Provider>
)}</Loading>
Expand Down
5 changes: 3 additions & 2 deletions packages/extension-ui/src/components/Box.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ function Box ({ children, className }: Props) {
}

export default styled(Box)`
background: transparent;
border-top: ${defaults.hdrBorder};
background: white;
border: ${defaults.boxBorder};
border-radius: ${defaults.borderRadius};
box-shadow: ${defaults.boxShadow};
color: ${defaults.color};
font-family: ${defaults.fontFamily};
font-size: ${defaults.fontSize};
Expand Down
1 change: 0 additions & 1 deletion packages/extension-ui/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ function Header ({ children, className, label }: Props) {

export default styled(Header)`
background: ${defaults.hdrBg};
border-top: ${defaults.hdrBorder};
box-sizing: border-box;
color: ${defaults.hdrColor};
font-family: ${defaults.fontFamily};
Expand Down
Loading

0 comments on commit f241e71

Please sign in to comment.