SMS Faucet (#4774)
* Faucet * Remove flakey button-index testing * Only display faucet when sms verified (mainnet) * simplify availability checks * WIP * Resuest from verified -> verified * Update endpoint, display response text * Error icon on errors * Parse hash text response * Use /api/:address endpoint * hash -> data * Adjust sms-certified message
This commit is contained in:
parent
4d08e7b0ae
commit
c3c83086bc
162
js/src/modals/Faucet/faucet.js
Normal file
162
js/src/modals/Faucet/faucet.js
Normal file
@ -0,0 +1,162 @@
|
||||
// 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 { observer } from 'mobx-react';
|
||||
import React, { Component, PropTypes } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { txLink } from '~/3rdparty/etherscan/links';
|
||||
import { Button, ModalBox, Portal, ShortenedHash } from '~/ui';
|
||||
import { CloseIcon, DialIcon, DoneIcon, ErrorIcon, SendIcon } from '~/ui/Icons';
|
||||
|
||||
import Store from './store';
|
||||
|
||||
@observer
|
||||
export default class Faucet extends Component {
|
||||
static propTypes = {
|
||||
address: PropTypes.string.isRequired,
|
||||
netVersion: PropTypes.string.isRequired,
|
||||
onClose: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
store = new Store(this.props.netVersion, this.props.address);
|
||||
|
||||
render () {
|
||||
const { error, isBusy, isCompleted } = this.store;
|
||||
|
||||
let icon = <DialIcon />;
|
||||
|
||||
if (isCompleted) {
|
||||
icon = error
|
||||
? <ErrorIcon />
|
||||
: <DoneIcon />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Portal
|
||||
buttons={ this.renderActions() }
|
||||
busy={ isBusy }
|
||||
isSmallModal
|
||||
onClose={ this.onClose }
|
||||
open
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='faucet.title'
|
||||
defaultMessage='Kovan ETH Faucet'
|
||||
/>
|
||||
}
|
||||
>
|
||||
<ModalBox
|
||||
icon={ icon }
|
||||
summary={
|
||||
isCompleted
|
||||
? this.renderSummaryDone()
|
||||
: this.renderSummaryRequest()
|
||||
}
|
||||
/>
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
|
||||
renderActions = () => {
|
||||
const { canTransact, isBusy, isCompleted } = this.store;
|
||||
|
||||
return isCompleted || isBusy
|
||||
? (
|
||||
<Button
|
||||
disabled={ isBusy }
|
||||
icon={ <DoneIcon /> }
|
||||
key='done'
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='faucet.buttons.done'
|
||||
defaultMessage='close'
|
||||
/>
|
||||
}
|
||||
onClick={ this.onClose }
|
||||
/>
|
||||
)
|
||||
: [
|
||||
<Button
|
||||
icon={ <CloseIcon /> }
|
||||
key='close'
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='faucet.buttons.close'
|
||||
defaultMessage='close'
|
||||
/>
|
||||
}
|
||||
onClick={ this.onClose }
|
||||
/>,
|
||||
<Button
|
||||
disabled={ !canTransact }
|
||||
icon={ <SendIcon /> }
|
||||
key='request'
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='faucet.buttons.request'
|
||||
defaultMessage='request'
|
||||
/>
|
||||
}
|
||||
onClick={ this.onExecute }
|
||||
/>
|
||||
];
|
||||
}
|
||||
|
||||
renderSummaryDone () {
|
||||
const { error, responseText, responseTxHash } = this.store;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<FormattedMessage
|
||||
id='faucet.summary.done'
|
||||
defaultMessage='Your Kovan ETH has been requested from the faucet which responded with -'
|
||||
/>
|
||||
{
|
||||
error
|
||||
? (
|
||||
<p>{ error }</p>
|
||||
)
|
||||
: (
|
||||
<p>
|
||||
<span>{ responseText } </span>
|
||||
<a href={ txLink(responseTxHash, false, '42') } target='_blank'>
|
||||
<ShortenedHash data={ responseTxHash } />
|
||||
</a>
|
||||
</p>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderSummaryRequest () {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id='faucet.summary.info'
|
||||
defaultMessage='To request a deposit of Kovan ETH to this address, you need to ensure that the address is sms-verified on the mainnet. Once executed the faucet will deposit Kovan ETH into the current account.'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
onClose = () => {
|
||||
this.props.onClose();
|
||||
}
|
||||
|
||||
onExecute = () => {
|
||||
return this.store.makeItRain();
|
||||
}
|
||||
}
|
17
js/src/modals/Faucet/index.js
Normal file
17
js/src/modals/Faucet/index.js
Normal 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 './faucet';
|
126
js/src/modals/Faucet/store.js
Normal file
126
js/src/modals/Faucet/store.js
Normal file
@ -0,0 +1,126 @@
|
||||
// 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 { action, computed, observable, transaction } from 'mobx';
|
||||
import apiutil from '~/api/util';
|
||||
|
||||
const ENDPOINT = 'http://faucet.kovan.network/api/';
|
||||
|
||||
export default class Store {
|
||||
@observable addressReceive = null;
|
||||
@observable addressVerified = null;
|
||||
@observable error = null;
|
||||
@observable responseText = null;
|
||||
@observable responseTxHash = null;
|
||||
@observable isBusy = false;
|
||||
@observable isCompleted = false;
|
||||
@observable isDestination = false;
|
||||
@observable isDone = false;
|
||||
|
||||
constructor (netVersion, address) {
|
||||
transaction(() => {
|
||||
this.setDestination(netVersion === '42');
|
||||
|
||||
this.setAddressReceive(address);
|
||||
this.setAddressVerified(address);
|
||||
});
|
||||
}
|
||||
|
||||
@computed get canTransact () {
|
||||
return !this.isBusy && this.addressReceiveValid && this.addressVerifiedValid;
|
||||
}
|
||||
|
||||
@computed get addressReceiveValid () {
|
||||
return apiutil.isAddressValid(this.addressReceive);
|
||||
}
|
||||
|
||||
@computed get addressVerifiedValid () {
|
||||
return apiutil.isAddressValid(this.addressVerified);
|
||||
}
|
||||
|
||||
@action setAddressReceive = (address) => {
|
||||
this.addressReceive = address;
|
||||
}
|
||||
|
||||
@action setAddressVerified = (address) => {
|
||||
this.addressVerified = address;
|
||||
}
|
||||
|
||||
@action setBusy = (isBusy) => {
|
||||
this.isBusy = isBusy;
|
||||
}
|
||||
|
||||
@action setCompleted = (isCompleted) => {
|
||||
transaction(() => {
|
||||
this.setBusy(false);
|
||||
this.isCompleted = isCompleted;
|
||||
});
|
||||
}
|
||||
|
||||
@action setDestination = (isDestination) => {
|
||||
this.isDestination = isDestination;
|
||||
}
|
||||
|
||||
@action setError = (error) => {
|
||||
if (error.indexOf('not certified') !== -1) {
|
||||
this.error = `${error}. Please ensure that this account is sms certified on the mainnet.`;
|
||||
} else {
|
||||
this.error = error;
|
||||
}
|
||||
}
|
||||
|
||||
@action setResponse = (response) => {
|
||||
this.responseText = response.result;
|
||||
this.responseTxHash = response.tx;
|
||||
}
|
||||
|
||||
makeItRain = () => {
|
||||
this.setBusy(true);
|
||||
|
||||
const options = {
|
||||
method: 'GET',
|
||||
mode: 'cors'
|
||||
};
|
||||
const url = `${ENDPOINT}${this.addressVerified}`;
|
||||
|
||||
return fetch(url, options)
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.catch(() => {
|
||||
return null;
|
||||
})
|
||||
.then((response) => {
|
||||
transaction(() => {
|
||||
if (!response || response.error) {
|
||||
this.setError(
|
||||
response
|
||||
? response.error
|
||||
: 'Unable to complete request to the faucet, the server may be unavailable. Please try again later.'
|
||||
);
|
||||
} else {
|
||||
this.setResponse(response);
|
||||
}
|
||||
|
||||
this.setCompleted(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@ -24,6 +24,7 @@ export DeleteAccount from './DeleteAccount';
|
||||
export DeployContract from './DeployContract';
|
||||
export EditMeta from './EditMeta';
|
||||
export ExecuteContract from './ExecuteContract';
|
||||
export Faucet from './Faucet';
|
||||
export FirstRun from './FirstRun';
|
||||
export LoadContract from './LoadContract';
|
||||
export PasswordManager from './PasswordManager';
|
||||
|
@ -31,6 +31,7 @@ export DashboardIcon from 'material-ui/svg-icons/action/dashboard';
|
||||
export DeleteIcon from 'material-ui/svg-icons/action/delete';
|
||||
export DevelopIcon from 'material-ui/svg-icons/action/description';
|
||||
export DoneIcon from 'material-ui/svg-icons/action/done-all';
|
||||
export DialIcon from 'material-ui/svg-icons/communication/dialpad';
|
||||
export EditIcon from 'material-ui/svg-icons/content/create';
|
||||
export ErrorIcon from 'material-ui/svg-icons/alert/error';
|
||||
export FileUploadIcon from 'material-ui/svg-icons/file/file-upload';
|
||||
|
@ -22,13 +22,13 @@ import styles from './modalBox.css';
|
||||
|
||||
export default class ModalBox extends Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
children: PropTypes.node,
|
||||
icon: PropTypes.node.isRequired,
|
||||
summary: nodeOrStringProptype()
|
||||
}
|
||||
|
||||
render () {
|
||||
const { children, icon } = this.props;
|
||||
const { icon } = this.props;
|
||||
|
||||
return (
|
||||
<div className={ styles.body }>
|
||||
@ -37,14 +37,26 @@ export default class ModalBox extends Component {
|
||||
</div>
|
||||
<div className={ styles.content }>
|
||||
{ this.renderSummary() }
|
||||
<div className={ styles.body }>
|
||||
{ children }
|
||||
</div>
|
||||
{ this.renderBody() }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderBody () {
|
||||
const { children } = this.props;
|
||||
|
||||
if (!children) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={ styles.body }>
|
||||
{ children }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
renderSummary () {
|
||||
const { summary } = this.props;
|
||||
|
||||
|
@ -22,11 +22,11 @@ import { bindActionCreators } from 'redux';
|
||||
|
||||
import shapeshiftBtn from '~/../assets/images/shapeshift-btn.png';
|
||||
import HardwareStore from '~/mobx/hardwareStore';
|
||||
import { EditMeta, DeleteAccount, Shapeshift, Verification, Transfer, PasswordManager } from '~/modals';
|
||||
import { DeleteAccount, EditMeta, Faucet, PasswordManager, Shapeshift, Transfer, Verification } from '~/modals';
|
||||
import { setVisibleAccounts } from '~/redux/providers/personalActions';
|
||||
import { fetchCertifiers, fetchCertifications } from '~/redux/providers/certifications/actions';
|
||||
import { Actionbar, Button, Page } from '~/ui';
|
||||
import { DeleteIcon, EditIcon, LockedIcon, SendIcon, VerifyIcon } from '~/ui/Icons';
|
||||
import { DeleteIcon, DialIcon, EditIcon, LockedIcon, SendIcon, VerifyIcon } from '~/ui/Icons';
|
||||
|
||||
import DeleteAddress from '../Address/Delete';
|
||||
|
||||
@ -48,6 +48,8 @@ class Account extends Component {
|
||||
|
||||
accounts: PropTypes.object,
|
||||
balances: PropTypes.object,
|
||||
certifications: PropTypes.object,
|
||||
netVersion: PropTypes.string.isRequired,
|
||||
params: PropTypes.object
|
||||
}
|
||||
|
||||
@ -97,6 +99,7 @@ class Account extends Component {
|
||||
<div>
|
||||
{ this.renderDeleteDialog(account) }
|
||||
{ this.renderEditDialog(account) }
|
||||
{ this.renderFaucetDialog() }
|
||||
{ this.renderFundDialog() }
|
||||
{ this.renderPasswordDialog(account) }
|
||||
{ this.renderTransferDialog(account, balance) }
|
||||
@ -117,8 +120,35 @@ class Account extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
isKovan = (netVersion) => {
|
||||
return netVersion === '42';
|
||||
}
|
||||
|
||||
isMainnet = (netVersion) => {
|
||||
return netVersion === '1';
|
||||
}
|
||||
|
||||
isFaucettable = (netVersion, certifications, address) => {
|
||||
return this.isKovan(netVersion) || (
|
||||
this.isMainnet(netVersion) &&
|
||||
this.isSmsCertified(certifications, address)
|
||||
);
|
||||
}
|
||||
|
||||
isSmsCertified = (_certifications, address) => {
|
||||
const certifications = _certifications && _certifications[address]
|
||||
? _certifications[address].filter((cert) => cert.name.indexOf('smsverification') === 0)
|
||||
: [];
|
||||
|
||||
return certifications.length !== 0;
|
||||
}
|
||||
|
||||
renderActionbar (account, balance) {
|
||||
const { certifications, netVersion } = this.props;
|
||||
const { address } = this.props.params;
|
||||
const showTransferButton = !!(balance && balance.tokens);
|
||||
const isVerifiable = this.isMainnet(netVersion);
|
||||
const isFaucettable = this.isFaucettable(netVersion, certifications, address);
|
||||
|
||||
const buttons = [
|
||||
<Button
|
||||
@ -149,17 +179,36 @@ class Account extends Component {
|
||||
}
|
||||
onClick={ this.store.toggleFundDialog }
|
||||
/>,
|
||||
<Button
|
||||
icon={ <VerifyIcon /> }
|
||||
key='sms-verification'
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='account.button.verify'
|
||||
defaultMessage='verify'
|
||||
isVerifiable
|
||||
? (
|
||||
<Button
|
||||
icon={ <VerifyIcon /> }
|
||||
key='verification'
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='account.button.verify'
|
||||
defaultMessage='verify'
|
||||
/>
|
||||
}
|
||||
onClick={ this.store.toggleVerificationDialog }
|
||||
/>
|
||||
}
|
||||
onClick={ this.store.toggleVerificationDialog }
|
||||
/>,
|
||||
)
|
||||
: null,
|
||||
isFaucettable
|
||||
? (
|
||||
<Button
|
||||
icon={ <DialIcon /> }
|
||||
key='faucet'
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='account.button.faucet'
|
||||
defaultMessage='Kovan ETH'
|
||||
/>
|
||||
}
|
||||
onClick={ this.store.toggleFaucetDialog }
|
||||
/>
|
||||
)
|
||||
: null,
|
||||
<Button
|
||||
icon={ <EditIcon /> }
|
||||
key='editmeta'
|
||||
@ -253,6 +302,24 @@ class Account extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
renderFaucetDialog () {
|
||||
const { netVersion } = this.props;
|
||||
|
||||
if (!this.store.isFaucetVisible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { address } = this.props.params;
|
||||
|
||||
return (
|
||||
<Faucet
|
||||
address={ address }
|
||||
netVersion={ netVersion }
|
||||
onClose={ this.store.toggleFaucetDialog }
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderFundDialog () {
|
||||
if (!this.store.isFundVisible) {
|
||||
return null;
|
||||
@ -317,10 +384,14 @@ class Account extends Component {
|
||||
function mapStateToProps (state) {
|
||||
const { accounts } = state.personal;
|
||||
const { balances } = state.balances;
|
||||
const certifications = state.certifications;
|
||||
const { netVersion } = state.nodeStatus;
|
||||
|
||||
return {
|
||||
accounts,
|
||||
balances
|
||||
balances,
|
||||
certifications,
|
||||
netVersion
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -80,57 +80,16 @@ describe('views/Account', () => {
|
||||
describe('sub-renderers', () => {
|
||||
describe('renderActionBar', () => {
|
||||
let bar;
|
||||
let barShallow;
|
||||
|
||||
beforeEach(() => {
|
||||
render();
|
||||
|
||||
bar = instance.renderActionbar({ tokens: {} });
|
||||
barShallow = shallow(bar);
|
||||
});
|
||||
|
||||
it('renders the bar', () => {
|
||||
expect(bar.type).to.match(/Actionbar/);
|
||||
});
|
||||
|
||||
// TODO: Finding by index is not optimal, however couldn't find a better method atm
|
||||
// since we cannot find by key (prop not visible in shallow debug())
|
||||
describe('clicks', () => {
|
||||
it('toggles transfer on click', () => {
|
||||
barShallow.find('Button').at(0).simulate('click');
|
||||
expect(store.isTransferVisible).to.be.true;
|
||||
});
|
||||
|
||||
it('toggles fund on click', () => {
|
||||
barShallow.find('Button').at(1).simulate('click');
|
||||
expect(store.isFundVisible).to.be.true;
|
||||
});
|
||||
|
||||
it('toggles fund on click', () => {
|
||||
barShallow.find('Button').at(1).simulate('click');
|
||||
expect(store.isFundVisible).to.be.true;
|
||||
});
|
||||
|
||||
it('toggles verify on click', () => {
|
||||
barShallow.find('Button').at(2).simulate('click');
|
||||
expect(store.isVerificationVisible).to.be.true;
|
||||
});
|
||||
|
||||
it('toggles edit on click', () => {
|
||||
barShallow.find('Button').at(3).simulate('click');
|
||||
expect(store.isEditVisible).to.be.true;
|
||||
});
|
||||
|
||||
it('toggles password on click', () => {
|
||||
barShallow.find('Button').at(4).simulate('click');
|
||||
expect(store.isPasswordVisible).to.be.true;
|
||||
});
|
||||
|
||||
it('toggles delete on click', () => {
|
||||
barShallow.find('Button').at(5).simulate('click');
|
||||
expect(store.isDeleteVisible).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderDeleteDialog', () => {
|
||||
|
@ -19,6 +19,7 @@ import { action, observable } from 'mobx';
|
||||
export default class Store {
|
||||
@observable isDeleteVisible = false;
|
||||
@observable isEditVisible = false;
|
||||
@observable isFaucetVisible = false;
|
||||
@observable isFundVisible = false;
|
||||
@observable isPasswordVisible = false;
|
||||
@observable isTransferVisible = false;
|
||||
@ -32,6 +33,10 @@ export default class Store {
|
||||
this.isEditVisible = !this.isEditVisible;
|
||||
}
|
||||
|
||||
@action toggleFaucetDialog = () => {
|
||||
this.isFaucetVisible = !this.isFaucetVisible;
|
||||
}
|
||||
|
||||
@action toggleFundDialog = () => {
|
||||
this.isFundVisible = !this.isFundVisible;
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ describe('views/Account/Store', () => {
|
||||
it('sets all modal visibility to false', () => {
|
||||
expect(store.isDeleteVisible).to.be.false;
|
||||
expect(store.isEditVisible).to.be.false;
|
||||
expect(store.isFaucetVisible).to.be.false;
|
||||
expect(store.isFundVisible).to.be.false;
|
||||
expect(store.isPasswordVisible).to.be.false;
|
||||
expect(store.isTransferVisible).to.be.false;
|
||||
@ -53,6 +54,13 @@ describe('views/Account/Store', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleFaucetDialog', () => {
|
||||
it('toggles the visibility', () => {
|
||||
store.toggleFaucetDialog();
|
||||
expect(store.isFaucetVisible).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleFundDialog', () => {
|
||||
it('toggles the visibility', () => {
|
||||
store.toggleFundDialog();
|
||||
|
Loading…
Reference in New Issue
Block a user