[beta] UI updates for 1.5.1 (#4429)

* s/Delete Contract/Forget Contract/ (#4237)

* Adjust the location of the signer snippet (#4155)

* Additional building-block UI components (#4239)

* Currency WIP

* Expand tests

* Pass className

* Add QrCode

* Export new components in ~/ui

* s/this.props.netSymbol/netSymbol/

* Fix import case

* ui/SectionList component (#4292)

* array chunking utility

* add SectionList component

* Add TODOs to indicate possible future work

* Add missing overlay style (as used in dapps at present)

* Add a Playground for the UI Components (#4301)

* Playground // WIP

* Linting

* Add Examples with code

* CSS Linting

* Linting

* Add Connected Currency Symbol

* 2015-2017

* 2015-2017

* 2015-2017

* 2015-2017

* 2015-2017

* 2015-2017

* 2015-2017

* Added `renderSymbol` tests

* PR grumbles

* Add Eth and Btc QRCode examples

* 2015-2017

* Add tests for playground

* Fixing tests

* Split Dapp icon into ui/DappIcon (#4308)

* Add QrCode & Copy to ShapeShift (#4322)

* Extract CopyIcon to ~/ui/Icons

* Add copy & QrCode address

* Default size 4

* Add bitcoin: link

* use protocol links applicable to coin exchanged

* Remove .only

* Display QrCode for accounts, addresses & contracts (#4329)

* Allow Portal to be used as top-level modal (#4338)

* Portal

* Allow Portal to be used in as both top-level and popover

* modal/popover variable naming

* export Portal in ~/ui

* Properly handle optional onKeyDown

* Add simple Playground Example

* Add proper event listener to Portal (#4359)

* Display AccountCard name via IdentityName (#4235)

* Fix signing (#4363)

* Dapp Account Selection & Defaults (#4355)

* Add parity_defaultAccount RPC (with subscription) (#4383)

* Default Account selector in Signer overlay (#4375)

* Typo, fixes #4271 (#4391)

* Fix ParityBar account selection overflows (#4405)

* Available Dapp selection alignment with Permissions (Portal) (#4374)

* registry dapp: make lookup use lower case (#4409)

* Dapps use defaultAccount instead of own selectors (#4386)

* Poll for defaultAccount to update dapp & overlay subscriptions (#4417)

* Poll for defaultAccount (Fixes #4413)

* Fix nextTimeout on catch

* Store timers

* Re-enable default updates on change detection

* Add block & timestamp conditions to Signer (#4411)

* Extension installation overlay (#4423)

* Extension installation overlay

* Pr gumbles

* Spelling

* Update Chrome URL

* Fix for non-included jsonrpc

* Extend Portal component (as per Modal) #4392
This commit is contained in:
Jaco Greeff
2017-02-04 09:42:36 +01:00
committed by Gav Wood
parent f76b94c2c5
commit fb817fcdca
136 changed files with 5775 additions and 1230 deletions

View File

@@ -15,15 +15,23 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.modal {
flex-direction: column;
}
.container {
overflow-y: auto;
}
.description {
margin-top: .5em !important;
}
.list {
margin-bottom: 1.5em;
.background {
background: rgba(255, 255, 255, 0.2);
margin: 0 -1.5em;
padding: 0.5em 1.5em;
padding: 0.5em 0;
}
.header {
@@ -37,3 +45,26 @@
opacity: 0.75;
}
}
.selectIcon {
position: absolute;
right: 0.5em;
top: 0.5em;
}
.selected,
.unselected {
position: relative;
}
.unselected {
background: rgba(0, 0, 0, 0.4) !important;
.selectIcon {
opacity: 0.15;
}
}
.selected {
background: rgba(255, 255, 255, 0.15) !important;
}

View File

@@ -1,4 +1,4 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
@@ -14,14 +14,12 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { Checkbox } from 'material-ui';
import { List, ListItem } from 'material-ui/List';
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { Modal, Button } from '~/ui';
import { DoneIcon } from '~/ui/Icons';
import { DappCard, Portal, SectionList } from '~/ui';
import { CheckIcon } from '~/ui/Icons';
import styles from './addDapps.css';
@@ -39,61 +37,61 @@ export default class AddDapps extends Component {
}
return (
<Modal
actions={ [
<Button
icon={ <DoneIcon /> }
key='done'
label={
<FormattedMessage
id='dapps.add.button.done'
defaultMessage='Done' />
}
onClick={ store.closeModal } />
] }
compact
<Portal
className={ styles.modal }
onClose={ store.closeModal }
open
title={
<FormattedMessage
id='dapps.add.label'
defaultMessage='visible applications' />
defaultMessage='visible applications'
/>
}
visible>
<div className={ styles.warning } />
{
this.renderList(store.sortedLocal,
<FormattedMessage
id='dapps.add.local.label'
defaultMessage='Applications locally available' />,
<FormattedMessage
id='dapps.add.local.desc'
defaultMessage='All applications installed locally on the machine by the user for access by the Parity client.' />
)
}
{
this.renderList(store.sortedBuiltin,
<FormattedMessage
id='dapps.add.builtin.label'
defaultMessage='Applications bundled with Parity' />,
<FormattedMessage
id='dapps.add.builtin.desc'
defaultMessage='Experimental applications developed by the Parity team to show off dapp capabilities, integration, experimental features and to control certain network-wide client behaviour.' />
)
}
{
this.renderList(store.sortedNetwork,
<FormattedMessage
id='dapps.add.network.label'
defaultMessage='Applications on the global network' />,
<FormattedMessage
id='dapps.add.network.desc'
defaultMessage='These applications are not affiliated with Parity nor are they published by Parity. Each remain under the control of their respective authors. Please ensure that you understand the goals for each application before interacting.' />
)
}
</Modal>
>
<div className={ styles.container }>
<div className={ styles.warning } />
{
this.renderList(store.sortedLocal, store.displayApps,
<FormattedMessage
id='dapps.add.local.label'
defaultMessage='Applications locally available'
/>,
<FormattedMessage
id='dapps.add.local.desc'
defaultMessage='All applications installed locally on the machine by the user for access by the Parity client.'
/>
)
}
{
this.renderList(store.sortedBuiltin, store.displayApps,
<FormattedMessage
id='dapps.add.builtin.label'
defaultMessage='Applications bundled with Parity'
/>,
<FormattedMessage
id='dapps.add.builtin.desc'
defaultMessage='Experimental applications developed by the Parity team to show off dapp capabilities, integration, experimental features and to control certain network-wide client behaviour.'
/>
)
}
{
this.renderList(store.sortedNetwork, store.displayApps,
<FormattedMessage
id='dapps.add.network.label'
defaultMessage='Applications on the global network'
/>,
<FormattedMessage
id='dapps.add.network.desc'
defaultMessage='These applications are not affiliated with Parity nor are they published by Parity. Each remain under the control of their respective authors. Please ensure that you understand the goals for each application before interacting.'
/>
)
}
</div>
</Portal>
);
}
renderList (items, header, byline) {
renderList (items, visibleItems, header, byline) {
if (!items || !items.length) {
return null;
}
@@ -104,41 +102,40 @@ export default class AddDapps extends Component {
<div className={ styles.header }>{ header }</div>
<div className={ styles.byline }>{ byline }</div>
</div>
<List>
{ items.map(this.renderApp) }
</List>
<SectionList
items={ items }
noStretch
renderItem={ this.renderApp }
/>
</div>
);
}
renderApp = (app) => {
const { store } = this.props;
const isHidden = !store.displayApps[app.id].visible;
const isVisible = store.displayApps[app.id].visible;
const onCheck = () => {
if (isHidden) {
store.showApp(app.id);
} else {
const onClick = () => {
if (isVisible) {
store.hideApp(app.id);
} else {
store.showApp(app.id);
}
};
return (
<ListItem
<DappCard
app={ app }
className={
isVisible
? styles.selected
: styles.unselected
}
key={ app.id }
leftCheckbox={
<Checkbox
checked={ !isHidden }
onCheck={ onCheck }
/>
}
primaryText={ app.name }
secondaryText={
<div className={ styles.description }>
{ app.description }
</div>
}
/>
onClick={ onClick }
>
<CheckIcon className={ styles.selectIcon } />
</DappCard>
);
}
}

View File

@@ -33,13 +33,13 @@ describe('modals/AddDapps', () => {
it('does not render the modal with modalOpen = false', () => {
expect(
renderShallow({ modalOpen: false }).find('Connect(Modal)')
renderShallow({ modalOpen: false }).find('Portal')
).to.have.length(0);
});
it('does render the modal with modalOpen = true', () => {
expect(
renderShallow({ modalOpen: true }).find('Connect(Modal)')
renderShallow({ modalOpen: true }).find('Portal')
).to.have.length(1);
});
});

View File

@@ -15,33 +15,54 @@
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.container {
overflow-y: auto;
}
.item {
.info {
display: inline-block;
display: flex;
flex: 1;
position: relative;
.address {
opacity: 0.75;
}
.description {
margin-top: 0.5em;
opacity: 0.75;
}
.name {
margin: 0.5em 0;
text-transform: uppercase;
}
.overlay {
position: absolute;
right: 0.5em;
top: 0.5em;
}
}
.selected, .unselected {
margin-bottom: 0.25em;
&:focus {
outline: none;
}
}
.unselected {
background: rgba(0, 0, 0, 0.4) !important;
}
.selected {
background: rgba(255, 255, 255, 0.15);
background: rgba(255, 255, 255, 0.15) !important;
&.default {
background: rgba(255, 255, 255, 0.35) !important;
}
}
.unselected {
}
.iconDisabled {
opacity: 0.15;
}
.legend {
opacity: 0.75;
span {
line-height: 24px;
vertical-align: top;
}
}

View File

@@ -1,4 +1,4 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
@@ -14,14 +14,12 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { Checkbox } from 'material-ui';
import { List, ListItem } from 'material-ui/List';
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { Button, IdentityIcon, Modal } from '~/ui';
import { DoneIcon } from '~/ui/Icons';
import { AccountCard, Portal, SectionList } from '~/ui';
import { CheckIcon, StarIcon, StarOutlineIcon } from '~/ui/Icons';
import styles from './dappPermissions.css';
@@ -39,74 +37,80 @@ export default class DappPermissions extends Component {
}
return (
<Modal
actions={ [
<Button
icon={ <DoneIcon /> }
key='done'
label={
<FormattedMessage
id='dapps.permissions.button.done'
defaultMessage='Done' />
}
onClick={ store.closeModal } />
] }
compact
<Portal
buttons={
<div className={ styles.legend }>
<FormattedMessage
id='dapps.permissions.description'
defaultMessage='{activeIcon} account is available to application, {defaultIcon} account is the default account'
values={ {
activeIcon: <CheckIcon />,
defaultIcon: <StarIcon />
} }
/>
</div>
}
onClose={ store.closeModal }
open
title={
<FormattedMessage
id='dapps.permissions.label'
defaultMessage='visible dapp accounts' />
defaultMessage='visible dapp accounts'
/>
}
visible>
<List>
{ this.renderListItems() }
</List>
</Modal>
>
<div className={ styles.container }>
<SectionList
items={ store.accounts }
noStretch
renderItem={ this.renderAccount }
/>
</div>
</Portal>
);
}
renderListItems () {
renderAccount = (account) => {
const { store } = this.props;
return store.accounts.map((account) => {
const onCheck = () => {
store.selectAccount(account.address);
};
const onMakeDefault = () => {
store.setDefaultAccount(account.address);
};
// TODO: Once new modal & account selection is in, this should be updated
// to conform to the new (as of this code WIP) look & feel for selection.
// For now in the current/old style, not as pretty but consistent.
return (
<ListItem
className={
const onSelect = () => {
store.selectAccount(account.address);
};
let className;
if (account.checked) {
className = account.default
? `${styles.selected} ${styles.default}`
: styles.selected;
} else {
className = styles.unselected;
}
return (
<div className={ styles.item }>
<AccountCard
account={ account }
className={ className }
onClick={ onSelect }
/>
<div className={ styles.overlay }>
{
account.checked && account.default
? <StarIcon />
: <StarOutlineIcon className={ styles.iconDisabled } onClick={ onMakeDefault } />
}
{
account.checked
? styles.selected
: styles.unselected
? <CheckIcon onClick={ onSelect } />
: <CheckIcon className={ styles.iconDisabled } onClick={ onSelect } />
}
key={ account.address }
leftCheckbox={
<Checkbox
checked={ account.checked }
onCheck={ onCheck }
/>
}
primaryText={
<div className={ styles.item }>
<IdentityIcon address={ account.address } />
<div className={ styles.info }>
<h3 className={ styles.name }>
{ account.name }
</h3>
<div className={ styles.address }>
{ account.address }
</div>
<div className={ styles.description }>
{ account.description }
</div>
</div>
</div>
} />
);
});
</div>
</div>
);
}
}

View File

@@ -33,13 +33,13 @@ describe('modals/DappPermissions', () => {
it('does not render the modal with modalOpen = false', () => {
expect(
renderShallow({ modalOpen: false }).find('Connect(Modal)')
renderShallow({ modalOpen: false }).find('Portal')
).to.have.length(0);
});
it('does render the modal with modalOpen = true', () => {
expect(
renderShallow({ modalOpen: true, accounts: [] }).find('Connect(Modal)')
renderShallow({ modalOpen: true, accounts: [] }).find('Portal')
).to.have.length(1);
});
});

View File

@@ -29,12 +29,17 @@ export default class Store {
@action closeModal = () => {
transaction(() => {
const accounts = this.accounts
.filter((account) => account.checked)
.map((account) => account.address);
let addresses = null;
const checkedAccounts = this.accounts.filter((account) => account.checked);
if (checkedAccounts.length) {
addresses = checkedAccounts.filter((account) => account.default)
.concat(checkedAccounts.filter((account) => !account.default))
.map((account) => account.address);
}
this.modalOpen = false;
this.updateWhitelist(accounts.length === this.accounts.length ? null : accounts);
this.updateWhitelist(addresses);
});
}
@@ -42,12 +47,15 @@ export default class Store {
transaction(() => {
this.accounts = Object
.values(accounts)
.map((account) => {
.map((account, index) => {
return {
address: account.address,
checked: this.whitelist
? this.whitelist.includes(account.address)
: true,
default: this.whitelist
? this.whitelist[0] === account.address
: index === 0,
description: account.meta.description,
name: account.name
};
@@ -57,9 +65,31 @@ export default class Store {
}
@action selectAccount = (address) => {
transaction(() => {
this.accounts = this.accounts.map((account) => {
if (account.address === address) {
account.checked = !account.checked;
account.default = false;
}
return account;
});
this.setDefaultAccount((
this.accounts.find((account) => account.default) ||
this.accounts.find((account) => account.checked) ||
{}
).address);
});
}
@action setDefaultAccount = (address) => {
this.accounts = this.accounts.map((account) => {
if (account.address === address) {
account.checked = !account.checked;
account.checked = true;
account.default = true;
} else if (account.default) {
account.default = false;
}
return account;

View File

@@ -23,21 +23,25 @@ const ACCOUNTS = {
'456': { address: '456', name: '456', meta: { description: '456' } },
'789': { address: '789', name: '789', meta: { description: '789' } }
};
const WHITELIST = ['123', '456'];
const WHITELIST = ['456', '789'];
let api;
let store;
function create () {
api = {
parity: {
getNewDappsWhitelist: sinon.stub().resolves(WHITELIST),
setNewDappsWhitelist: sinon.stub().resolves(true)
}
};
store = new Store(api);
}
describe('modals/DappPermissions/store', () => {
let api;
let store;
beforeEach(() => {
api = {
parity: {
getNewDappsWhitelist: sinon.stub().resolves(WHITELIST),
setNewDappsWhitelist: sinon.stub().resolves(true)
}
};
store = new Store(api);
create();
});
describe('constructor', () => {
@@ -51,49 +55,71 @@ describe('modals/DappPermissions/store', () => {
});
describe('@actions', () => {
describe('openModal', () => {
beforeEach(() => {
store.openModal(ACCOUNTS);
});
beforeEach(() => {
store.openModal(ACCOUNTS);
});
describe('openModal', () => {
it('sets the modalOpen status', () => {
expect(store.modalOpen).to.be.true;
});
it('sets accounts with checked interfaces', () => {
expect(store.accounts.peek()).to.deep.equal([
{ address: '123', name: '123', description: '123', checked: true },
{ address: '456', name: '456', description: '456', checked: true },
{ address: '789', name: '789', description: '789', checked: false }
{ address: '123', name: '123', description: '123', default: false, checked: false },
{ address: '456', name: '456', description: '456', default: true, checked: true },
{ address: '789', name: '789', description: '789', default: false, checked: true }
]);
});
});
describe('closeModal', () => {
beforeEach(() => {
store.openModal(ACCOUNTS);
store.selectAccount('789');
store.setDefaultAccount('789');
store.closeModal();
});
it('calls setNewDappsWhitelist', () => {
expect(api.parity.setNewDappsWhitelist).to.have.been.calledOnce;
});
it('has the default account in first position', () => {
expect(api.parity.setNewDappsWhitelist).to.have.been.calledWith(['789', '456']);
});
});
describe('selectAccount', () => {
beforeEach(() => {
store.openModal(ACCOUNTS);
store.selectAccount('123');
store.selectAccount('789');
});
it('unselects previous selected accounts', () => {
expect(store.accounts.find((account) => account.address === '123').checked).to.be.false;
expect(store.accounts.find((account) => account.address === '123').checked).to.be.true;
});
it('selects previous unselected accounts', () => {
expect(store.accounts.find((account) => account.address === '789').checked).to.be.true;
expect(store.accounts.find((account) => account.address === '789').checked).to.be.false;
});
it('sets a new default when default was unselected', () => {
store.selectAccount('456');
expect(store.accounts.find((account) => account.address === '456').default).to.be.false;
expect(store.accounts.find((account) => account.address === '123').default).to.be.true;
});
});
describe('setDefaultAccount', () => {
beforeEach(() => {
store.setDefaultAccount('789');
});
it('unselects previous default', () => {
expect(store.accounts.find((account) => account.address === '456').default).to.be.false;
});
it('selects new default', () => {
expect(store.accounts.find((account) => account.address === '789').default).to.be.true;
});
});
});

View File

@@ -1,4 +1,4 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
@@ -15,42 +15,22 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { Input, GasPriceEditor } from '~/ui';
import { GasPriceEditor } from '~/ui';
import styles from '../executeContract.css';
export default class AdvancedStep extends Component {
static propTypes = {
gasStore: PropTypes.object.isRequired,
minBlock: PropTypes.string,
minBlockError: PropTypes.string,
onMinBlockChange: PropTypes.func
gasStore: PropTypes.object.isRequired
};
render () {
const { gasStore, minBlock, minBlockError, onMinBlockChange } = this.props;
const { gasStore } = this.props;
return (
<div>
<Input
error={ minBlockError }
hint={
<FormattedMessage
id='executeContract.advanced.minBlock.hint'
defaultMessage='Only post the transaction after this block' />
}
label={
<FormattedMessage
id='executeContract.advanced.minBlock.label'
defaultMessage='BlockNumber to send from' />
}
value={ minBlock }
onSubmit={ onMinBlockChange } />
<div className={ styles.gaseditor }>
<GasPriceEditor store={ gasStore } />
</div>
<div className={ styles.gaseditor }>
<GasPriceEditor store={ gasStore } />
</div>
);
}

View File

@@ -1,4 +1,4 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
@@ -14,7 +14,6 @@
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import BigNumber from 'bignumber.js';
import { pick } from 'lodash';
import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
@@ -41,27 +40,32 @@ const TITLES = {
transfer: (
<FormattedMessage
id='executeContract.steps.transfer'
defaultMessage='function details' />
defaultMessage='function details'
/>
),
sending: (
<FormattedMessage
id='executeContract.steps.sending'
defaultMessage='sending' />
defaultMessage='sending'
/>
),
complete: (
<FormattedMessage
id='executeContract.steps.complete'
defaultMessage='complete' />
defaultMessage='complete'
/>
),
advanced: (
<FormattedMessage
id='executeContract.steps.advanced'
defaultMessage='advanced options' />
defaultMessage='advanced options'
/>
),
rejected: (
<FormattedMessage
id='executeContract.steps.rejected'
defaultMessage='rejected' />
defaultMessage='rejected'
/>
)
};
const STAGES_BASIC = [TITLES.transfer, TITLES.sending, TITLES.complete];
@@ -95,8 +99,6 @@ class ExecuteContract extends Component {
fromAddressError: null,
func: null,
funcError: null,
minBlock: '0',
minBlockError: null,
rejected: false,
sending: false,
step: STEP_DETAILS,
@@ -139,7 +141,8 @@ class ExecuteContract extends Component {
advancedOptions
? [STEP_BUSY]
: [STEP_BUSY_OR_ADVANCED]
}>
}
>
{ this.renderExceptionWarning() }
{ this.renderStep() }
</Modal>
@@ -161,8 +164,8 @@ class ExecuteContract extends Component {
renderDialogActions () {
const { onClose, fromAddress } = this.props;
const { advancedOptions, sending, step, fromAddressError, minBlockError, valuesError } = this.state;
const hasError = fromAddressError || minBlockError || valuesError.find((error) => error);
const { advancedOptions, sending, step, fromAddressError, valuesError } = this.state;
const hasError = fromAddressError || valuesError.find((error) => error);
const cancelBtn = (
<Button
@@ -170,10 +173,12 @@ class ExecuteContract extends Component {
label={
<FormattedMessage
id='executeContract.button.cancel'
defaultMessage='cancel' />
defaultMessage='cancel'
/>
}
icon={ <CancelIcon /> }
onClick={ onClose } />
onClick={ onClose }
/>
);
const postBtn = (
<Button
@@ -181,11 +186,13 @@ class ExecuteContract extends Component {
label={
<FormattedMessage
id='executeContract.button.post'
defaultMessage='post transaction' />
defaultMessage='post transaction'
/>
}
disabled={ !!(sending || hasError) }
icon={ <IdentityIcon address={ fromAddress } button /> }
onClick={ this.postTransaction } />
onClick={ this.postTransaction }
/>
);
const nextBtn = (
<Button
@@ -193,10 +200,12 @@ class ExecuteContract extends Component {
label={
<FormattedMessage
id='executeContract.button.next'
defaultMessage='next' />
defaultMessage='next'
/>
}
icon={ <NextIcon /> }
onClick={ this.onNextClick } />
onClick={ this.onNextClick }
/>
);
const prevBtn = (
<Button
@@ -204,10 +213,12 @@ class ExecuteContract extends Component {
label={
<FormattedMessage
id='executeContract.button.prev'
defaultMessage='prev' />
defaultMessage='prev'
/>
}
icon={ <PrevIcon /> }
onClick={ this.onPrevClick } />
onClick={ this.onPrevClick }
/>
);
if (step === STEP_DETAILS) {
@@ -233,16 +244,18 @@ class ExecuteContract extends Component {
label={
<FormattedMessage
id='executeContract.button.done'
defaultMessage='done' />
defaultMessage='done'
/>
}
icon={ <DoneIcon /> }
onClick={ onClose } />
onClick={ onClose }
/>
];
}
renderStep () {
const { onFromAddressChange } = this.props;
const { advancedOptions, step, busyState, minBlock, minBlockError, txhash, rejected } = this.state;
const { advancedOptions, step, busyState, txhash, rejected } = this.state;
if (rejected) {
return (
@@ -250,13 +263,16 @@ class ExecuteContract extends Component {
title={
<FormattedMessage
id='executeContract.rejected.title'
defaultMessage='The execution has been rejected' />
defaultMessage='The execution has been rejected'
/>
}
state={
<FormattedMessage
id='executeContract.rejected.state'
defaultMessage='You can safely close this window, the function execution will not occur.' />
} />
defaultMessage='You can safely close this window, the function execution will not occur.'
/>
}
/>
);
}
@@ -269,7 +285,8 @@ class ExecuteContract extends Component {
onFromAddressChange={ onFromAddressChange }
onFuncChange={ this.onFuncChange }
onAdvancedClick={ this.onAdvancedClick }
onValueChange={ this.onValueChange } />
onValueChange={ this.onValueChange }
/>
);
} else if (step === (advancedOptions ? STEP_BUSY : STEP_BUSY_OR_ADVANCED)) {
return (
@@ -277,17 +294,15 @@ class ExecuteContract extends Component {
title={
<FormattedMessage
id='executeContract.busy.title'
defaultMessage='The function execution is in progress' />
defaultMessage='The function execution is in progress'
/>
}
state={ busyState } />
state={ busyState }
/>
);
} else if (advancedOptions && (step === STEP_BUSY_OR_ADVANCED)) {
return (
<AdvancedStep
gasStore={ this.gasStore }
minBlock={ minBlock }
minBlockError={ minBlockError }
onMinBlockChange={ this.onMinBlockChange } />
<AdvancedStep gasStore={ this.gasStore } />
);
}
@@ -306,6 +321,7 @@ class ExecuteContract extends Component {
onFuncChange = (event, func) => {
const values = (func.abi.inputs || []).map((input) => {
const parsedType = parseAbiType(input.type);
return parsedType.default;
});
@@ -315,15 +331,6 @@ class ExecuteContract extends Component {
}, this.estimateGas);
}
onMinBlockChange = (minBlock) => {
const minBlockError = validateUint(minBlock).valueError;
this.setState({
minBlock,
minBlockError
});
}
onValueChange = (event, index, _value) => {
const { func, values, valuesError } = this.state;
const input = func.inputs.find((input, _index) => index === _index);
@@ -385,17 +392,14 @@ class ExecuteContract extends Component {
postTransaction = () => {
const { api, store } = this.context;
const { fromAddress } = this.props;
const { advancedOptions, amount, func, minBlock, values } = this.state;
const { advancedOptions, amount, func, values } = this.state;
const steps = advancedOptions ? STAGES_ADVANCED : STAGES_BASIC;
const finalstep = steps.length - 1;
const options = {
gas: this.gasStore.gas,
gasPrice: this.gasStore.price,
const options = this.gasStore.overrideTransaction({
from: fromAddress,
minBlock: new BigNumber(minBlock || 0).gt(0) ? minBlock : null,
value: api.util.toWei(amount || 0)
};
});
this.setState({ sending: true, step: advancedOptions ? STEP_BUSY : STEP_BUSY_OR_ADVANCED });
@@ -406,7 +410,8 @@ class ExecuteContract extends Component {
busyState: (
<FormattedMessage
id='executeContract.busy.waitAuth'
defaultMessage='Waiting for authorization in the Parity Signer' />
defaultMessage='Waiting for authorization in the Parity Signer'
/>
)
});
@@ -429,7 +434,8 @@ class ExecuteContract extends Component {
busyState: (
<FormattedMessage
id='executeContract.busy.posted'
defaultMessage='Your transaction has been posted to the network' />
defaultMessage='Your transaction has been posted to the network'
/>
)
});
})

View File

@@ -18,6 +18,8 @@ import { observer } from 'mobx-react';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import { CopyToClipboard, QrCode } from '~/ui';
import Value from '../Value';
import styles from '../shapeshift.css';
@@ -59,9 +61,7 @@ export default class AwaitingDepositStep extends Component {
typeSymbol
} } />
</div>
<div className={ styles.hero }>
{ depositAddress }
</div>
{ this.renderAddress(depositAddress, coinSymbol) }
<div className={ styles.price }>
<div>
<FormattedMessage
@@ -76,4 +76,42 @@ export default class AwaitingDepositStep extends Component {
</div>
);
}
renderAddress (depositAddress, coinSymbol) {
const qrcode = (
<QrCode
className={ styles.qrcode }
value={ depositAddress }
/>
);
let protocolLink = null;
// TODO: Expand for other coins where protocols are available
switch (coinSymbol) {
case 'BTC':
protocolLink = `bitcoin:${depositAddress}`;
break;
}
return (
<div className={ styles.addressInfo }>
{
protocolLink
? (
<a
href={ protocolLink }
target='_blank'
>
{ qrcode }
</a>
)
: qrcode
}
<div className={ styles.address }>
<CopyToClipboard data={ depositAddress } />
<span>{ depositAddress }</span>
</div>
</div>
);
}
}

View File

@@ -19,7 +19,10 @@ import React from 'react';
import AwaitingDepositStep from './';
const TEST_ADDRESS = '0x123456789123456789123456789123456789';
let component;
let instance;
function render () {
component = shallow(
@@ -29,6 +32,7 @@ function render () {
price: { rate: 0.001, minimum: 0, limit: 1.999 }
} } />
);
instance = component.instance();
return component;
}
@@ -47,4 +51,61 @@ describe('modals/Shapeshift/AwaitingDepositStep', () => {
render({ depositAddress: 'xyz' });
expect(component.find('FormattedMessage').first().props().id).to.match(/awaitingDeposit/);
});
describe('instance methods', () => {
describe('renderAddress', () => {
let address;
beforeEach(() => {
address = shallow(instance.renderAddress(TEST_ADDRESS));
});
it('renders the address', () => {
expect(address.text()).to.contain(TEST_ADDRESS);
});
describe('CopyToClipboard', () => {
let copy;
beforeEach(() => {
copy = address.find('Connect(CopyToClipboard)');
});
it('renders the copy', () => {
expect(copy.length).to.equal(1);
});
it('passes the address', () => {
expect(copy.props().data).to.equal(TEST_ADDRESS);
});
});
describe('QrCode', () => {
let qr;
beforeEach(() => {
qr = address.find('QrCode');
});
it('renders the QrCode', () => {
expect(qr.length).to.equal(1);
});
it('passed the address', () => {
expect(qr.props().value).to.equal(TEST_ADDRESS);
});
describe('protocol link', () => {
it('does not render a protocol link (unlinked type)', () => {
expect(address.find('a')).to.have.length(0);
});
it('renders protocol link for BTC', () => {
address = shallow(instance.renderAddress(TEST_ADDRESS, 'BTC'));
expect(address.find('a').props().href).to.equal(`bitcoin:${TEST_ADDRESS}`);
});
});
});
});
});
});

View File

@@ -14,9 +14,28 @@
/* You should have received a copy of the GNU General Public License
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
.body {
}
.addressInfo {
text-align: center;
.address {
background: rgba(255, 255, 255, 0.1);
margin: 0.75em 0;
padding: 1em;
span {
margin-left: 0.75em;
}
}
.qrcode {
margin: 0.75em 0;
}
}
.shapeshift {
position: absolute;
bottom: 0.5em;

View File

@@ -1,4 +1,4 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
@@ -27,37 +27,22 @@ export default class Extras extends Component {
dataError: PropTypes.string,
gasStore: PropTypes.object.isRequired,
isEth: PropTypes.bool,
minBlock: PropTypes.string,
minBlockError: PropTypes.string,
onChange: PropTypes.func.isRequired,
total: PropTypes.string,
totalError: PropTypes.string
}
render () {
const { gasStore, minBlock, minBlockError, onChange } = this.props;
const { gasStore, onChange } = this.props;
return (
<Form>
{ this.renderData() }
<Input
error={ minBlockError }
hint={
<FormattedMessage
id='transferModal.minBlock.hint'
defaultMessage='Only post the transaction after this block' />
}
label={
<FormattedMessage
id='transferModal.minBlock.label'
defaultMessage='BlockNumber to send from' />
}
value={ minBlock }
onChange={ this.onEditMinBlock } />
<div className={ styles.gaseditor }>
<GasPriceEditor
store={ gasStore }
onChange={ onChange } />
onChange={ onChange }
/>
</div>
</Form>
);
@@ -76,23 +61,22 @@ export default class Extras extends Component {
hint={
<FormattedMessage
id='transfer.advanced.data.hint'
defaultMessage='the data to pass through with the transaction' />
defaultMessage='the data to pass through with the transaction'
/>
}
label={
<FormattedMessage
id='transfer.advanced.data.label'
defaultMessage='transaction data' />
defaultMessage='transaction data'
/>
}
onChange={ this.onEditData }
value={ data } />
value={ data }
/>
);
}
onEditData = (event) => {
this.props.onChange('data', event.target.value);
}
onEditMinBlock = (event) => {
this.props.onChange('minBlock', event.target.value);
}
}

View File

@@ -1,4 +1,4 @@
// Copyright 2015, 2016 Parity Technologies (UK) Ltd.
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
@@ -52,9 +52,6 @@ export default class TransferStore {
@observable data = '';
@observable dataError = null;
@observable minBlock = '0';
@observable minBlockError = null;
@observable recipient = '';
@observable recipientError = ERRORS.requireRecipient;
@@ -78,6 +75,30 @@ export default class TransferStore {
gasStore = null;
constructor (api, props) {
this.api = api;
const { account, balance, gasLimit, senders, newError, sendersBalances } = props;
this.account = account;
this.balance = balance;
this.isWallet = account && account.wallet;
this.newError = newError;
this.gasStore = new GasPriceStore(api, { gasLimit });
if (this.isWallet) {
this.wallet = props.wallet;
this.walletContract = new Contract(this.api, walletAbi);
}
if (senders) {
this.senders = senders;
this.sendersBalances = sendersBalances;
this.senderError = ERRORS.requireSender;
}
}
@computed get steps () {
const steps = [].concat(this.extras ? STAGES_EXTRA : STAGES_BASIC);
@@ -90,7 +111,7 @@ export default class TransferStore {
@computed get isValid () {
const detailsValid = !this.recipientError && !this.valueError && !this.totalError && !this.senderError;
const extrasValid = !this.gasStore.errorGas && !this.gasStore.errorPrice && !this.minBlockError && !this.totalError;
const extrasValid = !this.gasStore.errorGas && !this.gasStore.errorPrice && !this.gasStore.conditionBlockError && !this.totalError;
const verifyValid = !this.passwordError;
switch (this.stage) {
@@ -111,29 +132,6 @@ export default class TransferStore {
return this.balance.tokens.find((balance) => balance.token.tag === this.tag).token;
}
constructor (api, props) {
this.api = api;
const { account, balance, gasLimit, senders, newError, sendersBalances } = props;
this.account = account;
this.balance = balance;
this.isWallet = account && account.wallet;
this.newError = newError;
this.gasStore = new GasPriceStore(api, { gasLimit });
if (this.isWallet) {
this.wallet = props.wallet;
this.walletContract = new Contract(this.api, walletAbi);
}
if (senders) {
this.senders = senders;
this.sendersBalances = sendersBalances;
this.senderError = ERRORS.requireSender;
}
}
@action onNext = () => {
this.stage += 1;
}
@@ -163,9 +161,6 @@ export default class TransferStore {
case 'gasPrice':
return this._onUpdateGasPrice(value);
case 'minBlock':
return this._onUpdateMinBlock(value);
case 'recipient':
return this._onUpdateRecipient(value);
@@ -283,14 +278,6 @@ export default class TransferStore {
this.recalculate();
}
@action _onUpdateMinBlock = (minBlock) => {
console.log('minBlock', minBlock);
transaction(() => {
this.minBlock = minBlock;
this.minBlockError = this._validatePositiveNumber(minBlock);
});
}
@action _onUpdateGasPrice = (gasPrice) => {
this.recalculate();
}
@@ -588,7 +575,7 @@ export default class TransferStore {
send () {
const { options, values } = this._getTransferParams();
options.minBlock = new BigNumber(this.minBlock || 0).gt(0) ? this.minBlock : null;
log.debug('@send', 'transfer value', options.value && options.value.toFormat());
return this._getTransferMethod().postTransaction(options, values);
@@ -596,6 +583,7 @@ export default class TransferStore {
_estimateGas (forceToken = false) {
const { options, values } = this._getTransferParams(true, forceToken);
return this._getTransferMethod(true, forceToken).estimateGas(options, values);
}
@@ -636,15 +624,12 @@ export default class TransferStore {
const to = (isEth && !isWallet) ? this.recipient
: (this.isWallet ? this.wallet.address : this.token.address);
const options = {
const options = this.gasStore.overrideTransaction({
from: this.sender || this.account.address,
to
};
});
if (!gas) {
options.gas = this.gasStore.gas;
options.gasPrice = this.gasStore.price;
} else {
if (gas) {
options.gas = MAX_GAS_ESTIMATION;
}
@@ -681,6 +666,7 @@ export default class TransferStore {
_validatePositiveNumber (num) {
try {
const v = new BigNumber(num);
if (v.lt(0)) {
return ERRORS.invalidAmount;
}

View File

@@ -206,7 +206,7 @@ class Transfer extends Component {
return null;
}
const { isEth, data, dataError, minBlock, minBlockError, total, totalError } = this.store;
const { isEth, data, dataError, total, totalError } = this.store;
return (
<Extras
@@ -214,8 +214,6 @@ class Transfer extends Component {
dataError={ dataError }
gasStore={ this.store.gasStore }
isEth={ isEth }
minBlock={ minBlock }
minBlockError={ minBlockError }
onChange={ this.store.onUpdateDetails }
total={ total }
totalError={ totalError } />

View File

@@ -16,10 +16,10 @@
import AddAddress from './AddAddress';
import AddContract from './AddContract';
import AddDapps from './AddDapps';
import CreateAccount from './CreateAccount';
import CreateWallet from './CreateWallet';
import DappPermissions from './DappPermissions';
import DappsVisible from './AddDapps';
import DeleteAccount from './DeleteAccount';
import DeployContract from './DeployContract';
import EditMeta from './EditMeta';
@@ -37,10 +37,10 @@ import WalletSettings from './WalletSettings';
export {
AddAddress,
AddContract,
AddDapps,
CreateAccount,
CreateWallet,
DappPermissions,
DappsVisible,
DeleteAccount,
DeployContract,
EditMeta,