implement basic badges/certifications/flair (#3665)

* sms verification: Certifications component

* sms verification: actions & reducers for certifications

* sms verification: put Certifications component into place

* sms verification: show certification icons

* sms verification: show certification titles

* sms verification: default icon for certifications

* sms verificaiton: lint issue 👕, fix testnet detection

The sms verification store got created when `isTestnet` (from the
Redux state) was still `undefined`.

* move certification helpers into middleware file

* connect Certifications to Redux

* don't pass certifications as prop

* move default certification icon into assets

* separate file for BadgeReg.sol

* don't pass certifications as prop

* Fix import name

* make BadgeReg a class

* make certifications middleware a class

* Certifications: pass in certifications of account
This commit is contained in:
Jannis Redmann 2016-11-30 21:39:06 +01:00 committed by Jaco Greeff
parent 837ff1bc7d
commit 784dcaff7c
18 changed files with 364 additions and 10 deletions

View File

@ -0,0 +1,4 @@
<svg width="100" height="100" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<circle fill="#4A90E2" cx="50" cy="50" r="50"/>
<path d="M20 45 L10 55 L35 85 L90 35 L80 25 L36 65 z" fill="#FFF"/>
</svg>

After

Width:  |  Height:  |  Size: 213 B

View File

@ -0,0 +1 @@
[{"constant":false,"inputs":[{"name":"_new","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_addr","type":"address"},{"name":"_name","type":"bytes32"}],"name":"register","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_name","type":"bytes32"}],"name":"fromName","outputs":[{"name":"id","type":"uint256"},{"name":"addr","type":"address"},{"name":"owner","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"badgeCount","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_fee","type":"uint256"}],"name":"setFee","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_id","type":"uint256"},{"name":"_key","type":"bytes32"}],"name":"meta","outputs":[{"name":"","type":"bytes32"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":false,"inputs":[],"name":"drain","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_id","type":"uint256"}],"name":"unregister","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_addr","type":"address"}],"name":"fromAddress","outputs":[{"name":"id","type":"uint256"},{"name":"name","type":"bytes32"},{"name":"owner","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_id","type":"uint256"}],"name":"badge","outputs":[{"name":"addr","type":"address"},{"name":"name","type":"bytes32"},{"name":"owner","type":"address"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_id","type":"uint256"},{"name":"_key","type":"bytes32"},{"name":"_value","type":"bytes32"}],"name":"setMeta","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_addr","type":"address"},{"name":"_name","type":"bytes32"},{"name":"_owner","type":"address"}],"name":"registerAs","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"fee","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"name","type":"bytes32"},{"indexed":true,"name":"id","type":"uint256"},{"indexed":false,"name":"addr","type":"address"}],"name":"Registered","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"name","type":"bytes32"},{"indexed":true,"name":"id","type":"uint256"}],"name":"Unregistered","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"id","type":"uint256"},{"indexed":true,"name":"key","type":"bytes32"},{"indexed":false,"name":"value","type":"bytes32"}],"name":"MetaChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"old","type":"address"},{"indexed":true,"name":"current","type":"address"}],"name":"NewOwner","type":"event"}]

View File

@ -0,0 +1 @@
[{"constant":true,"inputs":[{"name":"_who","type":"address"},{"name":"_field","type":"string"}],"name":"getAddress","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_who","type":"address"},{"name":"_field","type":"string"}],"name":"getUint","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_who","type":"address"}],"name":"certified","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_who","type":"address"},{"name":"_field","type":"string"}],"name":"get","outputs":[{"name":"","type":"bytes32"}],"payable":false,"type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"who","type":"address"}],"name":"Confirmed","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"who","type":"address"}],"name":"Revoked","type":"event"}]

View File

@ -14,6 +14,7 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with Parity. If not, see <http://www.gnu.org/licenses/>. // along with Parity. If not, see <http://www.gnu.org/licenses/>.
import badgereg from './badgereg.json';
import basiccoin from './basiccoin.json'; import basiccoin from './basiccoin.json';
import basiccoinmanager from './basiccoinmanager.json'; import basiccoinmanager from './basiccoinmanager.json';
import dappreg from './dappreg.json'; import dappreg from './dappreg.json';
@ -28,6 +29,7 @@ import tokenreg from './tokenreg.json';
import wallet from './wallet.json'; import wallet from './wallet.json';
export { export {
badgereg,
basiccoin, basiccoin,
basiccoinmanager, basiccoinmanager,
dappreg, dappreg,

View File

@ -0,0 +1,66 @@
// Copyright 2015, 2016 Ethcore (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 { bytesToHex, hex2Ascii } from '../api/util/format';
import ABI from './abi/certifier.json';
const ZERO = '0x0000000000000000000000000000000000000000000000000000000000000000';
export default class BadgeReg {
constructor (api, registry) {
this._api = api;
this._registry = registry;
registry.getContract('badgereg');
this.certifiers = {}; // by name
this.contracts = {}; // by name
}
fetchCertifier (name) {
if (this.certifiers[name]) {
return Promise.resolve(this.certifiers[name]);
}
return this._registry.getContract('badgereg')
.then((badgeReg) => {
return badgeReg.instance.fromName.call({}, [name])
.then(([ id, address ]) => {
return Promise.all([
badgeReg.instance.meta.call({}, [id, 'TITLE']),
badgeReg.instance.meta.call({}, [id, 'IMG'])
])
.then(([ title, img ]) => {
title = bytesToHex(title);
title = title === ZERO ? null : hex2Ascii(title);
if (bytesToHex(img) === ZERO) img = null;
const data = { address, name, title, icon: img };
this.certifiers[name] = data;
return data;
});
});
});
}
checkIfCertified (certifier, address) {
if (!this.contracts[certifier]) {
this.contracts[certifier] = this._api.newContract(ABI, certifier);
}
const contract = this.contracts[certifier];
return contract.instance.certified.call({}, [address]);
}
}

View File

@ -20,6 +20,7 @@ import SignatureReg from './signaturereg';
import TokenReg from './tokenreg'; import TokenReg from './tokenreg';
import GithubHint from './githubhint'; import GithubHint from './githubhint';
import * as smsVerification from './sms-verification'; import * as smsVerification from './sms-verification';
import BadgeReg from './badgereg';
let instance = null; let instance = null;
@ -33,6 +34,7 @@ export default class Contracts {
this._signaturereg = new SignatureReg(api, this._registry); this._signaturereg = new SignatureReg(api, this._registry);
this._tokenreg = new TokenReg(api, this._registry); this._tokenreg = new TokenReg(api, this._registry);
this._githubhint = new GithubHint(api, this._registry); this._githubhint = new GithubHint(api, this._registry);
this.badgeReg = new BadgeReg(api, this._registry);
} }
get registry () { get registry () {

View File

@ -20,17 +20,20 @@ import SettingsMiddleware from '../views/Settings/middleware';
import SignerMiddleware from './providers/signerMiddleware'; import SignerMiddleware from './providers/signerMiddleware';
import statusMiddleware from '../views/Status/middleware'; import statusMiddleware from '../views/Status/middleware';
import CertificationsMiddleware from './providers/certifications/middleware';
export default function (api) { export default function (api) {
const errors = new ErrorsMiddleware(); const errors = new ErrorsMiddleware();
const signer = new SignerMiddleware(api); const signer = new SignerMiddleware(api);
const settings = new SettingsMiddleware(); const settings = new SettingsMiddleware();
const status = statusMiddleware(); const status = statusMiddleware();
const certifications = new CertificationsMiddleware();
const middleware = [ const middleware = [
settings.toMiddleware(), settings.toMiddleware(),
signer.toMiddleware(), signer.toMiddleware(),
errors.toMiddleware() errors.toMiddleware(),
certifications.toMiddleware()
]; ];
return middleware.concat(status, thunk); return middleware.concat(status, thunk);

View File

@ -0,0 +1,23 @@
// Copyright 2015, 2016 Ethcore (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 const fetchCertifications = (address) => ({
type: 'fetchCertifications', address
});
export const addCertification = (address, name, title, icon) => ({
type: 'addCertification', address, name, title, icon
});

View File

@ -0,0 +1,51 @@
// Copyright 2015, 2016 Ethcore (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 Contracts from '../../../contracts';
import { addCertification } from './actions';
const knownCertifiers = [ 'smsverification' ];
export default class CertificationsMiddleware {
toMiddleware () {
return (store) => (next) => (action) => {
if (action.type !== 'fetchCertifications') {
return next(action);
}
const { address } = action;
const badgeReg = Contracts.get().badgeReg;
knownCertifiers.forEach((name) => {
badgeReg.fetchCertifier(name)
.then((cert) => {
return badgeReg.checkIfCertified(cert.address, address)
.then((isCertified) => {
if (isCertified) {
const { name, title, icon } = cert;
store.dispatch(addCertification(address, name, title, icon));
}
});
})
.catch((err) => {
if (err) {
console.error(`Failed to check if ${address} certified by ${name}:`, err);
}
});
});
};
}
}

View File

@ -0,0 +1,33 @@
// Copyright 2015, 2016 Ethcore (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/>.
const initialState = {};
export default (state = initialState, action) => {
if (action.type !== 'addCertification') {
return state;
}
const { address, name, icon, title } = action;
const certifications = state[address] || [];
if (certifications.some((c) => c.name === name)) {
return state;
}
const newCertifications = certifications.concat({ name, icon, title });
return { ...state, [address]: newCertifications };
};

View File

@ -18,6 +18,7 @@ import { combineReducers } from 'redux';
import { routerReducer } from 'react-router-redux'; import { routerReducer } from 'react-router-redux';
import { apiReducer, balancesReducer, blockchainReducer, compilerReducer, imagesReducer, personalReducer, signerReducer, statusReducer as nodeStatusReducer, snackbarReducer } from './providers'; import { apiReducer, balancesReducer, blockchainReducer, compilerReducer, imagesReducer, personalReducer, signerReducer, statusReducer as nodeStatusReducer, snackbarReducer } from './providers';
import certificationsReducer from './providers/certifications/reducer';
import errorReducer from '../ui/Errors/reducers'; import errorReducer from '../ui/Errors/reducers';
import settingsReducer from '../views/Settings/reducers'; import settingsReducer from '../views/Settings/reducers';
@ -32,6 +33,7 @@ export default function () {
settings: settingsReducer, settings: settingsReducer,
balances: balancesReducer, balances: balancesReducer,
certifications: certificationsReducer,
blockchain: blockchainReducer, blockchain: blockchainReducer,
compiler: compilerReducer, compiler: compilerReducer,
images: imagesReducer, images: imagesReducer,

View File

@ -0,0 +1,46 @@
/* Copyright 2015, 2016 Ethcore (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/>.
*/
.certifications {
margin-top: 1em;
}
.certification {
display: inline-block;
position: relative;
margin-right: .5em;
padding: .3em .6em .2em 2.6em;
border-radius: 1em;
line-height: 1em;
text-transform: uppercase;
background-color: rgba(255, 255, 255, 0.07);
}
.certification:last-child {
margin-right: 0;
}
.icon {
position: absolute;
top: -.25em;
left: 0;
width: 2em;
height: 2em;
margin: 0;
padding: 0;
border-radius: 1em;
}

View File

@ -0,0 +1,87 @@
// Copyright 2015, 2016 Ethcore (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 React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { hashToImageUrl } from '../../redux/providers/imagesReducer';
import { fetchCertifications } from '../../redux/providers/certifications/actions';
import defaultIcon from '../../../assets/images/certifications/unknown.svg';
import styles from './certifications.css';
class Certifications extends Component {
static propTypes = {
account: PropTypes.string.isRequired,
certifications: PropTypes.array.isRequired,
dappsUrl: PropTypes.string.isRequired,
fetchCertifications: PropTypes.func.isRequired
}
componentWillMount () {
const { account, fetchCertifications } = this.props;
fetchCertifications(account);
}
render () {
const { certifications } = this.props;
if (certifications.length === 0) {
return null;
}
return (
<div className={ styles.certifications }>
{ certifications.map(this.renderCertification) }
</div>
);
}
renderCertification = (certification) => {
const { name, title, icon } = certification;
const { dappsUrl } = this.props;
const classNames = `${styles.certification} ${!icon ? styles.noIcon : ''}`;
const img = icon ? dappsUrl + hashToImageUrl(icon) : defaultIcon;
return (
<div className={ classNames } key={ name }>
<img className={ styles.icon } src={ img } />
<div className={ styles.text }>{ title || name }</div>
</div>
);
}
}
function mapStateToProps (_, initProps) {
const { account } = initProps;
return (state) => {
const certifications = state.certifications[account] || [];
return { certifications };
};
}
function mapDispatchToProps (dispatch) {
return bindActionCreators({ fetchCertifications }, dispatch);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(Certifications);

View File

@ -0,0 +1,17 @@
// Copyright 2015, 2016 Ethcore (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 './certifications';

View File

@ -23,6 +23,7 @@ import Badge from './Badge';
import Balance from './Balance'; import Balance from './Balance';
import BlockStatus from './BlockStatus'; import BlockStatus from './BlockStatus';
import Button from './Button'; import Button from './Button';
import Certifications from './Certifications';
import ConfirmDialog from './ConfirmDialog'; import ConfirmDialog from './ConfirmDialog';
import Container, { Title as ContainerTitle } from './Container'; import Container, { Title as ContainerTitle } from './Container';
import ContextProvider from './ContextProvider'; import ContextProvider from './ContextProvider';
@ -55,6 +56,7 @@ export {
Balance, Balance,
BlockStatus, BlockStatus,
Button, Button,
Certifications,
ConfirmDialog, ConfirmDialog,
Container, Container,
ContainerTitle, ContainerTitle,

View File

@ -18,6 +18,7 @@ import React, { Component, PropTypes } from 'react';
import { Balance, Container, ContainerTitle, IdentityIcon, IdentityName, Tags } from '../../../ui'; import { Balance, Container, ContainerTitle, IdentityIcon, IdentityName, Tags } from '../../../ui';
import CopyToClipboard from '../../../ui/CopyToClipboard'; import CopyToClipboard from '../../../ui/CopyToClipboard';
import Certifications from '../../../ui/Certifications';
import styles from './header.css'; import styles from './header.css';
@ -32,6 +33,7 @@ export default class Header extends Component {
} }
render () { render () {
const { api } = this.context;
const { account, balance } = this.props; const { account, balance } = this.props;
const { address, meta, uuid } = account; const { address, meta, uuid } = account;
@ -67,6 +69,10 @@ export default class Header extends Component {
<Balance <Balance
account={ account } account={ account }
balance={ balance } /> balance={ balance } />
<Certifications
account={ account.address }
dappsUrl={ api.dappsUrl }
/>
</div> </div>
</Container> </Container>
</div> </div>

View File

@ -64,12 +64,6 @@ class Account extends Component {
} }
componentDidMount () { componentDidMount () {
const { api } = this.context;
const { address } = this.props.params;
const { isTestnet } = this.props;
const verificationStore = new VerificationStore(api, address, isTestnet);
this.setState({ verificationStore });
this.setVisibleAccounts(); this.setVisibleAccounts();
} }
@ -80,6 +74,15 @@ class Account extends Component {
if (prevAddress !== nextAddress) { if (prevAddress !== nextAddress) {
this.setVisibleAccounts(nextProps); this.setVisibleAccounts(nextProps);
} }
const { isTestnet } = nextProps;
if (typeof isTestnet === 'boolean' && !this.state.verificationStore) {
const { api } = this.context;
const { address } = nextProps.params;
this.setState({
verificationStore: new VerificationStore(api, address, isTestnet)
});
}
} }
componentWillUnmount () { componentWillUnmount () {
@ -115,7 +118,8 @@ class Account extends Component {
<Page> <Page>
<Header <Header
account={ account } account={ account }
balance={ balance } /> balance={ balance }
/>
<Transactions <Transactions
accounts={ accounts } accounts={ accounts }
address={ address } /> address={ address } />

View File

@ -132,7 +132,8 @@ class Contract extends Component {
<Page> <Page>
<Header <Header
account={ account } account={ account }
balance={ balance } /> balance={ balance }
/>
<Queries <Queries
contract={ contract } contract={ contract }
values={ queryValues } /> values={ queryValues } />
@ -447,7 +448,10 @@ function mapStateToProps (state) {
} }
function mapDispatchToProps (dispatch) { function mapDispatchToProps (dispatch) {
return bindActionCreators({ newError, setVisibleAccounts }, dispatch); return bindActionCreators({
newError,
setVisibleAccounts
}, dispatch);
} }
export default connect( export default connect(