Minimise transactions progress (#4942)

* Watch the requests and display them throughout the app

* Linting

* Showing Requests

* Fully working Transaction Requests Display

* Add FormattedMessage to Requests

* Clean-up the Transfer dialog

* Update Validations

* Cleanup Create Wallet

* Clean Deploy Contract Dialog

* Cleanup Contract Execution

* Fix Requests

* Cleanup Wallet Settings

* Don't show stepper in Portal if less than 2 steps

* WIP local storage requests

* Caching requests and saving contract deployments

* Add Historic prop to Requests MethodDecoding

* Fix tests

* Add Contract address to MethodDecoding

* PR Grumbles - Part I

* PR Grumbles - Part II

* Use API Subscription methods

* Linting

* Move SavedRequests and add tests

* Added tests for Requests Actions

* Fixing tests

* PR Grumbles + Playground fix

* Revert Playground changes

* PR Grumbles

* Better showEth in MethodDecoding
This commit is contained in:
Nicolas Gotchac
2017-03-28 14:34:31 +02:00
committed by Jaco Greeff
parent e28c477075
commit a99721004b
40 changed files with 1382 additions and 1216 deletions

View File

@@ -0,0 +1,17 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
export default from './requests';

View File

@@ -0,0 +1,127 @@
/* Copyright 2015-2017 Parity Technologies (UK) Ltd.
/* This file is part of Parity.
/*
/* Parity is free software: you can redistribute it and/or modify
/* it under the terms of the GNU General Public License as published by
/* the Free Software Foundation, either version 3 of the License, or
/* (at your option) any later version.
/*
/* Parity is distributed in the hope that it will be useful,
/* but WITHOUT ANY WARRANTY; without even the implied warranty of
/* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
/* GNU General Public License for more details.
/*
/* You should have received a copy of the GNU General Public License
/* along with Parity. If not, see <http://www.gnu.org/licenses/>.
*/
$baseColor: 255;
$baseOpacity: 0.95;
.requests {
align-items: flex-end;
bottom: 2em;
display: flex;
flex-direction: column;
position: fixed;
right: 0.175em;
z-index: 750;
* {
font-size: 0.85rem !important;
}
}
.request {
animation-fill-mode: forwards;
animation-timing-function: cubic-bezier(0.7, 0, 0.3, 1);
background-color: rgba($baseColor, $baseColor, $baseColor, $baseOpacity);
color: black;
cursor: pointer;
margin-top: 0.5em;
opacity: 1;
&.hide {
animation-duration: 0.5s;
animation-name: fadeout;
}
.status {
padding: 0.5em;
&.error {
background-color: rgba(200, 40, 40, 0.95);
color: white;
}
}
.container {
display: flex;
flex-direction: row;
padding: 1em;
* {
color: black !important;
}
}
&:hover .container {
background-color: rgba($baseColor, $baseColor, $baseColor, 1);
}
p {
margin: 0;
}
}
@keyframes fadeout {
from {
display: block;
height: inherit;
opacity: 1;
}
49% {
display: block;
height: inherit;
opacity: 0;
}
98% {
display: block;
height: 0;
opacity: 0;
}
to {
display: none;
height: 0;
opacity: 0;
}
}
.identity {
align-items: center;
display: flex;
flex-direction: column;
justify-content: center;
margin-right: 1em;
.icon {
margin-bottom: 0.5rem;
}
}
.inline {
align-items: center;
display: flex;
flex-direction: row;
.fill {
flex: 1 0 auto;
}
}
.hash {
margin-left: 0.25em;
}

View File

@@ -0,0 +1,233 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import { LinearProgress } from 'material-ui';
import React, { Component, PropTypes } from 'react';
import { FormattedMessage } from 'react-intl';
import ReactDOM from 'react-dom';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { hideRequest } from '~/redux/providers/requestsActions';
import { MethodDecoding, IdentityIcon, ScrollableText, ShortenedHash } from '~/ui';
import styles from './requests.css';
const ERROR_STATE = 'ERROR_STATE';
const DONE_STATE = 'DONE_STATE';
const WAITING_STATE = 'WAITING_STATE';
class Requests extends Component {
static propTypes = {
requests: PropTypes.object.isRequired,
onHideRequest: PropTypes.func.isRequired
};
state = {
extras: {}
};
render () {
const { requests } = this.props;
const { extras } = this.state;
return (
<div className={ styles.requests }>
{ Object.values(requests).map((request) => this.renderRequest(request, extras[request.requestId])) }
</div>
);
}
renderRequest (request, extras = {}) {
const { show, transaction } = request;
const state = this.getTransactionState(request);
const displayedTransaction = { ...transaction };
// Don't show gas and gasPrice
delete displayedTransaction.gas;
delete displayedTransaction.gasPrice;
const requestClasses = [ styles.request ];
const statusClasses = [ styles.status ];
const requestStyle = {};
const handleHideRequest = () => {
this.handleHideRequest(request.requestId);
};
if (state.type === ERROR_STATE) {
statusClasses.push(styles.error);
}
if (!show) {
requestClasses.push(styles.hide);
}
// Set the Request height (for animation) if found
if (extras.height) {
requestStyle.height = extras.height;
}
return (
<div
className={ requestClasses.join(' ') }
key={ request.requestId }
ref={ `request_${request.requestId}` }
onClick={ handleHideRequest }
style={ requestStyle }
>
<div className={ statusClasses.join(' ') }>
{ this.renderStatus(request) }
</div>
{
state.type === ERROR_STATE
? null
: (
<LinearProgress
max={ 6 }
mode={ state.type === WAITING_STATE ? 'indeterminate' : 'determinate' }
value={ state.type === DONE_STATE ? request.blockHeight.toNumber() : 6 }
/>
)
}
<div className={ styles.container }>
<div className={ styles.identity } title={ transaction.from }>
<IdentityIcon
address={ transaction.from }
inline
center
className={ styles.icon }
/>
</div>
<MethodDecoding
address={ transaction.from }
compact
historic={ state.type === DONE_STATE }
transaction={ displayedTransaction }
/>
</div>
</div>
);
}
renderStatus (request) {
const { error, transactionHash, transactionReceipt } = request;
if (error) {
return (
<div
className={ styles.inline }
title={ error.message }
>
<FormattedMessage
id='requests.status.error'
defaultMessage='An error occured:'
/>
<div className={ styles.fill }>
<ScrollableText
text={ error.text || error.message }
/>
</div>
</div>
);
}
if (transactionReceipt) {
return (
<FormattedMessage
id='requests.status.transactionMined'
defaultMessage='Transaction mined at block #{blockNumber} ({blockHeight} blocks ago)'
values={ {
blockHeight: request.blockHeight.toNumber(),
blockNumber: transactionReceipt.blockNumber.toFormat()
} }
/>
);
}
if (transactionHash) {
return (
<div className={ styles.inline }>
<FormattedMessage
id='requests.status.transactionSent'
defaultMessage='Transaction sent to network with hash'
/>
<div className={ [ styles.fill, styles.hash ].join(' ') }>
<ShortenedHash data={ transactionHash } />
</div>
</div>
);
}
return (
<FormattedMessage
id='requests.status.waitingForSigner'
defaultMessage='Waiting for authorization in the Parity Signer'
/>
);
}
getTransactionState (request) {
const { error, transactionReceipt } = request;
if (error) {
return { type: ERROR_STATE };
}
if (transactionReceipt) {
return { type: DONE_STATE };
}
return { type: WAITING_STATE };
}
handleHideRequest = (requestId) => {
const requestElement = ReactDOM.findDOMNode(this.refs[`request_${requestId}`]);
// Try to get the request element height, to have a nice transition effect
if (requestElement) {
const { height } = requestElement.getBoundingClientRect();
const prevExtras = this.state.extras;
const nextExtras = {
...prevExtras,
[ requestId ]: {
...prevExtras[requestId],
height
}
};
return this.setState({ extras: nextExtras }, () => {
return this.props.onHideRequest(requestId);
});
}
return this.props.onHideRequest(requestId);
}
}
const mapStateToProps = (state) => {
const { requests } = state;
return { requests };
};
function mapDispatchToProps (dispatch) {
return bindActionCreators({
onHideRequest: hideRequest
}, dispatch);
}
export default connect(mapStateToProps, mapDispatchToProps)(Requests);

View File

@@ -0,0 +1,89 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import store from 'store';
import { ERROR_CODES } from '~/api/transport/error';
export const LS_REQUESTS_KEY = '_parity::requests';
export default class SavedRequests {
load (api) {
const requests = this._get();
const promises = Object.values(requests).map((request) => {
const { requestId, transactionHash } = request;
// The request hasn't been signed yet
if (transactionHash) {
return request;
}
return this._requestExists(api, requestId)
.then((exists) => {
if (!exists) {
return null;
}
return request;
})
.catch(() => {
this.remove(requestId);
});
});
return Promise.all(promises).then((requests) => requests.filter((request) => request));
}
save (requestId, requestData) {
const requests = this._get();
requests[requestId] = {
...(requests[requestId] || {}),
...requestData
};
this._set(requests);
}
remove (requestId) {
const requests = this._get();
delete requests[requestId];
this._set(requests);
}
_get () {
return store.get(LS_REQUESTS_KEY) || {};
}
_set (requests = {}) {
return store.set(LS_REQUESTS_KEY, requests);
}
_requestExists (api, requestId) {
return api.parity
.checkRequest(requestId)
.then(() => true)
.catch((error) => {
if (error.code === ERROR_CODES.REQUEST_NOT_FOUND) {
return false;
}
throw error;
});
}
}

View File

@@ -0,0 +1,88 @@
// Copyright 2015-2017 Parity Technologies (UK) Ltd.
// This file is part of Parity.
// Parity is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Parity is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
import sinon from 'sinon';
import store from 'store';
import SavedRequests, { LS_REQUESTS_KEY } from './savedRequests';
const DEFAULT_REQUEST = {
requestId: '0x1',
transaction: {}
};
const api = createApi();
const savedRequests = new SavedRequests();
function createApi () {
return {
parity: {
checkRequest: sinon.stub().resolves()
}
};
}
describe('views/Application/Requests/savedRequests', () => {
beforeEach(() => {
store.set(LS_REQUESTS_KEY, {
[DEFAULT_REQUEST.requestId]: DEFAULT_REQUEST
});
});
afterEach(() => {
store.set(LS_REQUESTS_KEY, {});
});
it('gets requests from local storage', () => {
const requests = savedRequests._get();
expect(requests[DEFAULT_REQUEST.requestId]).to.deep.equal(DEFAULT_REQUEST);
});
it('sets requests to local storage', () => {
savedRequests._set({});
const requests = savedRequests._get();
expect(requests).to.deep.equal({});
});
it('removes requests', () => {
savedRequests.remove(DEFAULT_REQUEST.requestId);
const requests = savedRequests._get();
expect(requests).to.deep.equal({});
});
it('saves new requests', () => {
savedRequests.save(DEFAULT_REQUEST.requestId, { extraData: true });
const requests = savedRequests._get();
expect(requests[DEFAULT_REQUEST.requestId]).to.deep.equal({
...DEFAULT_REQUEST,
extraData: true
});
});
it('loads requests', () => {
return savedRequests.load(api)
.then((requests) => {
expect(requests[0]).to.deep.equal(DEFAULT_REQUEST);
});
});
});

View File

@@ -31,6 +31,7 @@ import FrameError from './FrameError';
import Status from './Status';
import Store from './store';
import TabBar from './TabBar';
import Requests from './Requests';
import styles from './application.css';
@@ -103,6 +104,7 @@ class Application extends Component {
}
<Extension />
<Snackbar />
<Requests />
</Container>
);
}