From 784dcaff7ca34b71690bdcb61be3bfb1e0a666d0 Mon Sep 17 00:00:00 2001 From: Jannis Redmann Date: Wed, 30 Nov 2016 21:39:06 +0100 Subject: [PATCH] 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 :shirt:, 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 --- js/assets/images/certifications/unknown.svg | 4 + js/src/contracts/abi/badgereg.json | 1 + js/src/contracts/abi/certifier.json | 1 + js/src/contracts/abi/index.js | 2 + js/src/contracts/badgereg.js | 66 ++++++++++++++ js/src/contracts/contracts.js | 2 + js/src/redux/middleware.js | 5 +- .../redux/providers/certifications/actions.js | 23 +++++ .../providers/certifications/middleware.js | 51 +++++++++++ .../redux/providers/certifications/reducer.js | 33 +++++++ js/src/redux/reducers.js | 2 + js/src/ui/Certifications/certifications.css | 46 ++++++++++ js/src/ui/Certifications/certifications.js | 87 +++++++++++++++++++ js/src/ui/Certifications/index.js | 17 ++++ js/src/ui/index.js | 2 + js/src/views/Account/Header/header.js | 6 ++ js/src/views/Account/account.js | 18 ++-- js/src/views/Contract/contract.js | 8 +- 18 files changed, 364 insertions(+), 10 deletions(-) create mode 100644 js/assets/images/certifications/unknown.svg create mode 100644 js/src/contracts/abi/badgereg.json create mode 100644 js/src/contracts/abi/certifier.json create mode 100644 js/src/contracts/badgereg.js create mode 100644 js/src/redux/providers/certifications/actions.js create mode 100644 js/src/redux/providers/certifications/middleware.js create mode 100644 js/src/redux/providers/certifications/reducer.js create mode 100644 js/src/ui/Certifications/certifications.css create mode 100644 js/src/ui/Certifications/certifications.js create mode 100644 js/src/ui/Certifications/index.js diff --git a/js/assets/images/certifications/unknown.svg b/js/assets/images/certifications/unknown.svg new file mode 100644 index 000000000..1554bcc25 --- /dev/null +++ b/js/assets/images/certifications/unknown.svg @@ -0,0 +1,4 @@ + + + + diff --git a/js/src/contracts/abi/badgereg.json b/js/src/contracts/abi/badgereg.json new file mode 100644 index 000000000..3d18ba393 --- /dev/null +++ b/js/src/contracts/abi/badgereg.json @@ -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"}] \ No newline at end of file diff --git a/js/src/contracts/abi/certifier.json b/js/src/contracts/abi/certifier.json new file mode 100644 index 000000000..905ddde6c --- /dev/null +++ b/js/src/contracts/abi/certifier.json @@ -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"}] \ No newline at end of file diff --git a/js/src/contracts/abi/index.js b/js/src/contracts/abi/index.js index a6a7f0783..f15765b1a 100644 --- a/js/src/contracts/abi/index.js +++ b/js/src/contracts/abi/index.js @@ -14,6 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . +import badgereg from './badgereg.json'; import basiccoin from './basiccoin.json'; import basiccoinmanager from './basiccoinmanager.json'; import dappreg from './dappreg.json'; @@ -28,6 +29,7 @@ import tokenreg from './tokenreg.json'; import wallet from './wallet.json'; export { + badgereg, basiccoin, basiccoinmanager, dappreg, diff --git a/js/src/contracts/badgereg.js b/js/src/contracts/badgereg.js new file mode 100644 index 000000000..f8dbefa78 --- /dev/null +++ b/js/src/contracts/badgereg.js @@ -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 . + +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]); + } +} diff --git a/js/src/contracts/contracts.js b/js/src/contracts/contracts.js index cefece7de..f61a63690 100644 --- a/js/src/contracts/contracts.js +++ b/js/src/contracts/contracts.js @@ -20,6 +20,7 @@ import SignatureReg from './signaturereg'; import TokenReg from './tokenreg'; import GithubHint from './githubhint'; import * as smsVerification from './sms-verification'; +import BadgeReg from './badgereg'; let instance = null; @@ -33,6 +34,7 @@ export default class Contracts { this._signaturereg = new SignatureReg(api, this._registry); this._tokenreg = new TokenReg(api, this._registry); this._githubhint = new GithubHint(api, this._registry); + this.badgeReg = new BadgeReg(api, this._registry); } get registry () { diff --git a/js/src/redux/middleware.js b/js/src/redux/middleware.js index a54f2fae4..c48c64673 100644 --- a/js/src/redux/middleware.js +++ b/js/src/redux/middleware.js @@ -20,17 +20,20 @@ import SettingsMiddleware from '../views/Settings/middleware'; import SignerMiddleware from './providers/signerMiddleware'; import statusMiddleware from '../views/Status/middleware'; +import CertificationsMiddleware from './providers/certifications/middleware'; export default function (api) { const errors = new ErrorsMiddleware(); const signer = new SignerMiddleware(api); const settings = new SettingsMiddleware(); const status = statusMiddleware(); + const certifications = new CertificationsMiddleware(); const middleware = [ settings.toMiddleware(), signer.toMiddleware(), - errors.toMiddleware() + errors.toMiddleware(), + certifications.toMiddleware() ]; return middleware.concat(status, thunk); diff --git a/js/src/redux/providers/certifications/actions.js b/js/src/redux/providers/certifications/actions.js new file mode 100644 index 000000000..c84f7db55 --- /dev/null +++ b/js/src/redux/providers/certifications/actions.js @@ -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 . + +export const fetchCertifications = (address) => ({ + type: 'fetchCertifications', address +}); + +export const addCertification = (address, name, title, icon) => ({ + type: 'addCertification', address, name, title, icon +}); diff --git a/js/src/redux/providers/certifications/middleware.js b/js/src/redux/providers/certifications/middleware.js new file mode 100644 index 000000000..c2f36931c --- /dev/null +++ b/js/src/redux/providers/certifications/middleware.js @@ -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 . + +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); + } + }); + }); + }; + } +} diff --git a/js/src/redux/providers/certifications/reducer.js b/js/src/redux/providers/certifications/reducer.js new file mode 100644 index 000000000..87b0dfd33 --- /dev/null +++ b/js/src/redux/providers/certifications/reducer.js @@ -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 . + +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 }; +}; diff --git a/js/src/redux/reducers.js b/js/src/redux/reducers.js index 45119d289..3c7a10fe9 100644 --- a/js/src/redux/reducers.js +++ b/js/src/redux/reducers.js @@ -18,6 +18,7 @@ import { combineReducers } from 'redux'; import { routerReducer } from 'react-router-redux'; 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 settingsReducer from '../views/Settings/reducers'; @@ -32,6 +33,7 @@ export default function () { settings: settingsReducer, balances: balancesReducer, + certifications: certificationsReducer, blockchain: blockchainReducer, compiler: compilerReducer, images: imagesReducer, diff --git a/js/src/ui/Certifications/certifications.css b/js/src/ui/Certifications/certifications.css new file mode 100644 index 000000000..077830e3a --- /dev/null +++ b/js/src/ui/Certifications/certifications.css @@ -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 . +*/ + +.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; +} diff --git a/js/src/ui/Certifications/certifications.js b/js/src/ui/Certifications/certifications.js new file mode 100644 index 000000000..d2f06398e --- /dev/null +++ b/js/src/ui/Certifications/certifications.js @@ -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 . + +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 ( +
+ { certifications.map(this.renderCertification) } +
+ ); + } + + 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 ( +
+ +
{ title || name }
+
+ ); + } +} + +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); diff --git a/js/src/ui/Certifications/index.js b/js/src/ui/Certifications/index.js new file mode 100644 index 000000000..02719ee31 --- /dev/null +++ b/js/src/ui/Certifications/index.js @@ -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 . + +export default from './certifications'; diff --git a/js/src/ui/index.js b/js/src/ui/index.js index 4c16bc4a2..543d7c763 100644 --- a/js/src/ui/index.js +++ b/js/src/ui/index.js @@ -23,6 +23,7 @@ import Badge from './Badge'; import Balance from './Balance'; import BlockStatus from './BlockStatus'; import Button from './Button'; +import Certifications from './Certifications'; import ConfirmDialog from './ConfirmDialog'; import Container, { Title as ContainerTitle } from './Container'; import ContextProvider from './ContextProvider'; @@ -55,6 +56,7 @@ export { Balance, BlockStatus, Button, + Certifications, ConfirmDialog, Container, ContainerTitle, diff --git a/js/src/views/Account/Header/header.js b/js/src/views/Account/Header/header.js index b28abeb69..808998c17 100644 --- a/js/src/views/Account/Header/header.js +++ b/js/src/views/Account/Header/header.js @@ -18,6 +18,7 @@ import React, { Component, PropTypes } from 'react'; import { Balance, Container, ContainerTitle, IdentityIcon, IdentityName, Tags } from '../../../ui'; import CopyToClipboard from '../../../ui/CopyToClipboard'; +import Certifications from '../../../ui/Certifications'; import styles from './header.css'; @@ -32,6 +33,7 @@ export default class Header extends Component { } render () { + const { api } = this.context; const { account, balance } = this.props; const { address, meta, uuid } = account; @@ -67,6 +69,10 @@ export default class Header extends Component { + diff --git a/js/src/views/Account/account.js b/js/src/views/Account/account.js index e27333cbf..21d2f380c 100644 --- a/js/src/views/Account/account.js +++ b/js/src/views/Account/account.js @@ -64,12 +64,6 @@ class Account extends Component { } 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(); } @@ -80,6 +74,15 @@ class Account extends Component { if (prevAddress !== nextAddress) { 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 () { @@ -115,7 +118,8 @@ class Account extends Component {
+ balance={ balance } + /> diff --git a/js/src/views/Contract/contract.js b/js/src/views/Contract/contract.js index 613bf70b9..54d06f228 100644 --- a/js/src/views/Contract/contract.js +++ b/js/src/views/Contract/contract.js @@ -132,7 +132,8 @@ class Contract extends Component {
+ balance={ balance } + /> @@ -447,7 +448,10 @@ function mapStateToProps (state) { } function mapDispatchToProps (dispatch) { - return bindActionCreators({ newError, setVisibleAccounts }, dispatch); + return bindActionCreators({ + newError, + setVisibleAccounts + }, dispatch); } export default connect(