feat: sellers' arbitration selection screen (#351)

* feat: show no arbitrator warning
* feat: sellers' arbitration selection screen
This commit is contained in:
Richard Ramos 2019-07-05 08:42:17 -04:00
parent 438c0d5317
commit af5ea83969
11 changed files with 248 additions and 21 deletions

View File

@ -1,5 +1,6 @@
import {ARBITRATION_UNSOLVED, GET_DISPUTED_ESCROWS, RESOLVE_DISPUTE, RESOLVE_DISPUTE_FAILED, BUY_LICENSE, CHECK_LICENSE_OWNER, LOAD_PRICE, LOAD_ARBITRATION, GET_ARBITRATORS, OPEN_DISPUTE, CANCEL_DISPUTE} from './constants';
import {ARBITRATION_UNSOLVED, GET_DISPUTED_ESCROWS, RESOLVE_DISPUTE, RESOLVE_DISPUTE_FAILED, BUY_LICENSE,CANCEL_ARBITRATOR_SELECTION_ACTIONS, CHECK_LICENSE_OWNER, LOAD_PRICE, LOAD_ARBITRATION, GET_ARBITRATORS, OPEN_DISPUTE, CANCEL_DISPUTE, REQUEST_ARBITRATOR, CANCEL_ARBITRATOR_REQUEST} from './constants';
import Escrow from '../../../embarkArtifacts/contracts/Escrow';
import ArbitrationLicense from '../../../embarkArtifacts/contracts/ArbitrationLicense';
import OwnedUpgradeabilityProxy from '../../../embarkArtifacts/contracts/OwnedUpgradeabilityProxy';
Escrow.options.address = OwnedUpgradeabilityProxy.options.address;
@ -28,10 +29,16 @@ export const loadArbitration = (escrowId) => {
return {type: LOAD_ARBITRATION, escrowId};
};
export const getArbitrators = (address) => ({type: GET_ARBITRATORS, address});
export const getArbitrators = (address, includeAll) => ({type: GET_ARBITRATORS, address, includeAll});
export const buyLicense = () => ({ type: BUY_LICENSE });
export const loadPrice = () => ({ type: LOAD_PRICE });
export const checkLicenseOwner = () => ({ type: CHECK_LICENSE_OWNER });
export const requestArbitrator = (arbitrator) => ({ type: REQUEST_ARBITRATOR, arbitrator, toSend: ArbitrationLicense.methods.requestArbitrator(arbitrator) });
export const cancelArbitratorRequest = (arbitrator) => ({type: CANCEL_ARBITRATOR_REQUEST, arbitrator});
export const cancelArbitratorActions = () => ({type: CANCEL_ARBITRATOR_SELECTION_ACTIONS});

View File

@ -43,3 +43,20 @@ export const CHECK_LICENSE_OWNER = 'CHECK_ARB_LICENSE_OWNER';
export const CHECK_LICENSE_OWNER_SUCCEEDED = 'CHECK_ARB_LICENSE_OWNER_SUCCEEDED';
export const CHECK_LICENSE_OWNER_FAILED = 'CHECK_ARB_LICENSE_OWNER_FAILED';
export const REQUEST_ARBITRATOR = 'REQUEST_ARBITRATOR';
export const REQUEST_ARBITRATOR_PRE_SUCCESS = 'REQUEST_ARBITRATOR_PRE_SUCCESS';
export const REQUEST_ARBITRATOR_SUCCEEDED = 'REQUEST_ARBITRATOR_SUCCEEDED';
export const REQUEST_ARBITRATOR_FAILED = 'REQUEST_ARBITRATOR_FAILED';
export const CANCEL_ARBITRATOR_REQUEST = 'CANCEL_ARBITRATOR_REQUEST';
export const CANCEL_ARBITRATOR_REQUEST_PRE_SUCCESS = 'CANCEL_ARBITRATOR_REQUEST_PRE_SUCCESS';
export const CANCEL_ARBITRATOR_REQUEST_SUCCEEDED = 'CANCEL_ARBITRATOR_REQUEST_SUCCEEDED';
export const CANCEL_ARBITRATOR_REQUEST_FAILED = 'CANCEL_ARBITRATOR_REQUEST_FAILED';
export const CANCEL_ARBITRATOR_SELECTION_ACTIONS = 'CANCEL_ARBITRATOR_SELECTION_ACTIONS';
export const NONE = '0';
export const AWAIT = '1';
export const ACCEPTED = '2';
export const REJECTED = '3';
export const CLOSED = '4';

View File

@ -2,5 +2,6 @@ import saga from './saga';
import reducer from './reducer';
import * as actions from './actions';
import * as selectors from './selectors';
import * as constants from './constants';
export default {saga, reducer, actions, selectors};
export default {saga, reducer, actions, selectors, constants};

View File

@ -27,14 +27,24 @@ import {
LOAD_PRICE_SUCCEEDED,
CHECK_LICENSE_OWNER_FAILED,
CHECK_LICENSE_OWNER_SUCCEEDED,
ARBITRATION_UNSOLVED
ARBITRATION_UNSOLVED,
REQUEST_ARBITRATOR_PRE_SUCCESS,
REQUEST_ARBITRATOR_SUCCEEDED,
REQUEST_ARBITRATOR_FAILED,
REQUEST_ARBITRATOR,
CANCEL_ARBITRATOR_SELECTION_ACTIONS,
AWAIT,
CANCEL_ARBITRATOR_REQUEST_PRE_SUCCESS,
CANCEL_ARBITRATOR_REQUEST_FAILED,
CANCEL_ARBITRATOR_REQUEST_SUCCEEDED,
CLOSED
} from './constants';
import { fromTokenDecimals } from '../../utils/numbers';
import {RESET_STATE, PURGE_STATE} from "../network/constants";
import {toChecksumAddress, zeroAddress} from '../../utils/address';
import {zeroAddress} from '../../utils/address';
const DEFAULT_STATE = {
escrows: [], arbitration: null, arbitrators: [], licenseOwner: false,
escrows: [], arbitration: null, arbitrators: {}, licenseOwner: false,
receipt: null,
price: Number.MAX_SAFE_INTEGER,
loading: false,
@ -53,6 +63,8 @@ function reducer(state = DEFAULT_STATE, action) {
case CANCEL_DISPUTE_PRE_SUCCESS:
case OPEN_DISPUTE_PRE_SUCCESS:
case RESOLVE_DISPUTE_PRE_SUCCESS:
case REQUEST_ARBITRATOR_PRE_SUCCESS:
case CANCEL_ARBITRATOR_REQUEST_PRE_SUCCESS:
return {
...state, ...{
txHash: action.txHash
@ -92,12 +104,15 @@ function reducer(state = DEFAULT_STATE, action) {
case GET_DISPUTED_ESCROWS_FAILED:
case RESOLVE_DISPUTE_FAILED:
case GET_ARBITRATORS_FAILED:
case REQUEST_ARBITRATOR_FAILED:
case CANCEL_ARBITRATOR_REQUEST_FAILED:
return {
...state, ...{
errorGet: action.error,
loading: false
}
};
case REQUEST_ARBITRATOR:
case CANCEL_DISPUTE:
case OPEN_DISPUTE:
case RESOLVE_DISPUTE:
@ -133,7 +148,7 @@ function reducer(state = DEFAULT_STATE, action) {
case GET_ARBITRATORS_SUCCEEDED:
return {
...state,
arbitrators: action.arbitrators.map(arbitratorAddr => toChecksumAddress(arbitratorAddr))
arbitrators: action.arbitrators
};
case BUY_LICENSE:
return {
@ -153,11 +168,35 @@ function reducer(state = DEFAULT_STATE, action) {
loading: false,
error: ''
};
case REQUEST_ARBITRATOR_SUCCEEDED:
{
const arbitrators = {...state.arbitrators};
arbitrators[action.arbitrator].request.status = AWAIT;
return {
...state,
arbitrators,
loading: false,
error: ''
};
}
case CANCEL_ARBITRATOR_REQUEST_SUCCEEDED:
{
const arbitrators = {...state.arbitrators};
arbitrators[action.arbitrator].request.status = CLOSED;
return {
...state,
arbitrators,
loading: false,
error: ''
};
}
case CANCEL_ARBITRATOR_SELECTION_ACTIONS:
case BUY_LICENSE_CANCEL:
return {
...state,
loading: false,
error: ''
error: '',
errorGet: ''
};
case LOAD_PRICE_SUCCEEDED:
return {

View File

@ -13,7 +13,7 @@ import {
RESOLVE_DISPUTE_PRE_SUCCESS, LOAD_ARBITRATION, LOAD_ARBITRATION_FAILED, LOAD_ARBITRATION_SUCCEEDED, GET_ARBITRATORS,
GET_ARBITRATORS_SUCCEEDED, GET_ARBITRATORS_FAILED, BUY_LICENSE, BUY_LICENSE_FAILED, BUY_LICENSE_PRE_SUCCESS, BUY_LICENSE_SUCCEEDED,
LOAD_PRICE, LOAD_PRICE_FAILED, LOAD_PRICE_SUCCEEDED, CHECK_LICENSE_OWNER, CHECK_LICENSE_OWNER_FAILED, CHECK_LICENSE_OWNER_SUCCEEDED,
OPEN_DISPUTE, OPEN_DISPUTE_SUCCEEDED, OPEN_DISPUTE_FAILED, OPEN_DISPUTE_PRE_SUCCESS, CANCEL_DISPUTE, CANCEL_DISPUTE_PRE_SUCCESS, CANCEL_DISPUTE_SUCCEEDED, CANCEL_DISPUTE_FAILED
OPEN_DISPUTE, OPEN_DISPUTE_SUCCEEDED, OPEN_DISPUTE_FAILED, OPEN_DISPUTE_PRE_SUCCESS, CANCEL_DISPUTE, CANCEL_DISPUTE_PRE_SUCCESS, CANCEL_DISPUTE_SUCCEEDED, CANCEL_DISPUTE_FAILED, REQUEST_ARBITRATOR, REQUEST_ARBITRATOR_PRE_SUCCESS, REQUEST_ARBITRATOR_SUCCEEDED, REQUEST_ARBITRATOR_FAILED, CANCEL_ARBITRATOR_REQUEST, CANCEL_ARBITRATOR_REQUEST_SUCCEEDED, CANCEL_ARBITRATOR_REQUEST_FAILED, CANCEL_ARBITRATOR_REQUEST_PRE_SUCCESS
} from './constants';
import OwnedUpgradeabilityProxy from '../../../embarkArtifacts/contracts/OwnedUpgradeabilityProxy';
Escrow.options.address = OwnedUpgradeabilityProxy.options.address;
@ -30,14 +30,19 @@ export function *onCancelDispute() {
yield takeEvery(CANCEL_DISPUTE, doTransaction.bind(null, CANCEL_DISPUTE_PRE_SUCCESS, CANCEL_DISPUTE_SUCCEEDED, CANCEL_DISPUTE_FAILED));
}
export function *doGetArbitrators({address}) {
export function *doGetArbitrators({address, includeAll}) {
try {
const cnt = yield call(ArbitrationLicense.methods.getNumLicenseOwners().call);
const arbitrators = [];
const arbitrators = {};
for(let i = 0; i < cnt; i++){
const arbitrator = yield call(ArbitrationLicense.methods.licenseOwners(i).call);
const arbitrator = web3.utils.toChecksumAddress(yield call(ArbitrationLicense.methods.licenseOwners(i).call));
const isAllowed = yield call(ArbitrationLicense.methods.isAllowed(address, arbitrator).call);
if(isAllowed) arbitrators.push(arbitrator);
if(isAllowed || includeAll) {
const id = web3.utils.soliditySha3(arbitrator, address);
arbitrators[arbitrator] = yield call(ArbitrationLicense.methods.arbitratorlicenseDetails(arbitrator).call);
arbitrators[arbitrator].isAllowed = isAllowed;
arbitrators[arbitrator].request = yield call(ArbitrationLicense.methods.requests(id).call);
}
}
yield put({type: GET_ARBITRATORS_SUCCEEDED, arbitrators});
} catch (error) {
@ -170,4 +175,33 @@ export function *onCheckLicenseOwner() {
yield takeEvery(CHECK_LICENSE_OWNER, doCheckLicenseOwner);
}
export default [fork(onGetEscrows), fork(onResolveDispute), fork(onLoadArbitration), fork(onGetArbitrators), fork(onBuyLicense), fork(onCheckLicenseOwner), fork(onLoadPrice), fork(onOpenDispute), fork(onCancelDispute)];
export function *onRequestArbitrator() {
yield takeEvery(REQUEST_ARBITRATOR, doTransaction.bind(null, REQUEST_ARBITRATOR_PRE_SUCCESS, REQUEST_ARBITRATOR_SUCCEEDED, REQUEST_ARBITRATOR_FAILED));
}
export function *doCancelArbitratorRequest({arbitrator}){
const id = web3.utils.soliditySha3(arbitrator, web3.eth.defaultAccount);
const toSend = ArbitrationLicense.methods.cancelRequest(id);
yield doTransaction(CANCEL_ARBITRATOR_REQUEST_PRE_SUCCESS, CANCEL_ARBITRATOR_REQUEST_SUCCEEDED, CANCEL_ARBITRATOR_REQUEST_FAILED, {
arbitrator,
toSend
});
}
export function *onCancelArbitratorRequest() {
yield takeEvery(CANCEL_ARBITRATOR_REQUEST, doCancelArbitratorRequest);
}
export default [
fork(onGetEscrows),
fork(onResolveDispute),
fork(onLoadArbitration),
fork(onGetArbitrators),
fork(onBuyLicense),
fork(onCheckLicenseOwner),
fork(onLoadPrice),
fork(onOpenDispute),
fork(onCancelDispute),
fork(onRequestArbitrator),
fork(onCancelArbitratorRequest)
];

View File

@ -1,6 +1,7 @@
/* global web3 */
import MetadataStore from '../../../embarkArtifacts/contracts/MetadataStore';
import ArbitrationLicense from '../../../embarkArtifacts/contracts/ArbitrationLicense';
import SellerLicense from '../../../embarkArtifacts/contracts/SellerLicense';
import Escrow from '../../../embarkArtifacts/contracts/Escrow';
import {fork, takeEvery, put, all} from 'redux-saga/effects';
@ -16,15 +17,18 @@ import {USER_RATING, LOAD_ESCROWS} from '../escrow/constants';
import {doTransaction} from '../../utils/saga';
import {getLocation} from '../../services/googleMap';
import OwnedUpgradeabilityProxy from '../../../embarkArtifacts/contracts/OwnedUpgradeabilityProxy';
import { zeroAddress, addressCompare } from '../../utils/address';
Escrow.options.address = OwnedUpgradeabilityProxy.options.address;
export function *loadUser({address}) {
try {
const isArbitrator = yield ArbitrationLicense.methods.isLicenseOwner(address).call();
const isSeller = yield SellerLicense.methods.isLicenseOwner(address).call();
const isUser = yield MetadataStore.methods.userWhitelist(address).call();
let user = {
isArbitrator
isArbitrator,
isSeller
};
if (!isUser){
@ -89,6 +93,11 @@ export function *loadOffers({address}) {
const offers = yield all(offerIds.map(function *(id) {
const offer = yield MetadataStore.methods.offer(id).call();
if(!addressCompare(offer.arbitrator, zeroAddress)){
const id = yield MetadataStore.methods.addressToUser(offer.arbitrator).call();
offer.arbitratorData = yield MetadataStore.methods.users(id).call();
}
if (!loadedUsers.includes(offer.owner)) {
loadedUsers.push(offer.owner);
yield put({type: LOAD_USER, address: offer.owner});

View File

@ -17,6 +17,7 @@ import Profile from '../pages/Profile';
import Escrow from '../pages/Escrow';
import OpenDispute from '../pages/OpenDispute';
import Arbitration from '../pages/Arbitration';
import Arbitrators from '../pages/Arbitrators';
import MyProfile from '../pages/MyProfile';
import EditMyContact from '../pages/EditMyContact';
import License from '../pages/License';
@ -129,6 +130,7 @@ class App extends Component {
<Route exact path="/license" component={License}/>
<Route exact path="/escrow/:id" component={Escrow}/>
<Route exact path="/arbitration/:id" component={Arbitration}/>
<Route exact path="/arbitrators" component={Arbitrators} />
<Route exact path="/openCase/:id" component={OpenDispute}/>
<Route exact path="/offers/list" component={OffersList}/>

View File

@ -0,0 +1,116 @@
import React, { Component, Fragment } from 'react';
import { connect } from 'react-redux';
import {withRouter} from "react-router-dom";
import { ListGroup, ListGroupItem, Button, Row, Col } from 'reactstrap';
import classnames from 'classnames';
import metadata from '../../features/metadata';
import network from '../../features/network';
import arbitration from '../../features/arbitration';
import { addressCompare } from '../../utils/address';
import Loading from '../../components/Loading';
import ErrorInformation from '../../components/ErrorInformation';
import moment from 'moment';
import PropTypes from 'prop-types';
class Arbitrators extends Component {
constructor(props) {
super(props);
props.getArbitrators(props.address, true);
this.loadedUsers = [];
}
componentDidUpdate(prevProps) {
if ((!prevProps.arbitrators && this.props.arbitrators) || prevProps.arbitrators.length !== this.props.arbitrators.length || Object.keys(this.props.users).length < this.props.arbitrators.length) {
Object.keys(this.props.arbitrators).forEach(arbitratorAddr => {
if (!this.props.users[arbitratorAddr] && !this.loadedUsers.includes(arbitratorAddr)) {
this.props.getUser(arbitratorAddr);
this.loadedUsers.push(arbitratorAddr);
}
});
}
}
requestArbitrator = (arbitrator) => () => {
this.props.requestArbitrator(arbitrator);
}
cancelRequest = (arbitrator) => () => {
this.props.cancelArbitratorRequest(arbitrator);
}
render(){
const {arbitrators, users, loading, error, txHash, address, cancelArbitratorsActions} = this.props;
if(error) {
return <ErrorInformation transaction message={error} cancel={cancelArbitratorsActions}/>;
}
if(loading) return <Loading mining={true} txHash={txHash} />;
return (
<Fragment>
<h2 className="mb-4">Arbitrators</h2>
<p>Here you can see a list of arbitrators that have approved you as a seller and request approval by those that have not.</p>
<ListGroup>
{Object.keys(arbitrators).map((arb, i) => {
const isUser = addressCompare(address, arb);
const enableDate = parseInt(arbitrators[arb].request.date, 10) + 86420;
const isDisabled = (Date.now() / 1000) < enableDate;
return <ListGroupItem key={i}>
<Row>
<Col xs="12" sm="9" className="pb-3">
<span className="text-small">{arb}</span>
<br />
<span className={classnames("font-weight-bold", {'text-success': isUser})}>{(users[arb] && users[arb].username ? users[arb].username : "Arbitrator has no username") + (isUser ? " (You)" : "") }</span>
</Col>
<Col xs="12" sm="3" className="text-center">
{ !isUser && !arbitrators[arb].isAllowed && [arbitration.constants.NONE, arbitration.constants.REJECTED, arbitration.constants.CLOSED].indexOf(arbitrators[arb].request.status) > -1 && <Button disabled={isDisabled} onClick={this.requestArbitrator(arb)}>Request</Button> }
{ arbitrators[arb].isAllowed && !isUser && <span className="text-success">Available</span> }
{ !isUser && arbitrators[arb].request.status === arbitration.constants.AWAIT && <Button onClick={this.cancelRequest(arb)}>Cancel request</Button> }
{ !isUser && isDisabled && <span className="text-small text-muted">Retry in {moment(enableDate * 1000).toNow(true)}</span>}
</Col>
</Row>
</ListGroupItem>;
})}
</ListGroup>
</Fragment>
);
}
}
Arbitrators.propTypes = {
arbitrators: PropTypes.object,
users: PropTypes.object,
address: PropTypes.string,
loading: PropTypes.bool,
error: PropTypes.string,
txHash: PropTypes.string,
getArbitrators: PropTypes.func,
getUser: PropTypes.func,
requestArbitrator: PropTypes.func,
cancelArbitratorsActions: PropTypes.func,
cancelArbitratorRequest: PropTypes.func
};
const mapStateToProps = state => {
const address = network.selectors.getAddress(state) || '';
return {
address,
arbitrators: arbitration.selectors.arbitrators(state),
users: metadata.selectors.getAllUsers(state),
loading: arbitration.selectors.isLoading(state),
error: arbitration.selectors.errorGet(state),
txHash: arbitration.selectors.txHash(state)
};
};
export default connect(
mapStateToProps,
{
getArbitrators: arbitration.actions.getArbitrators,
getUser: metadata.actions.loadUserOnly,
requestArbitrator: arbitration.actions.requestArbitrator,
cancelArbitratorsActions: arbitration.actions.cancelArbitratorActions,
cancelArbitratorRequest: arbitration.actions.cancelArbitratorRequest
})(withRouter(Arbitrators));

View File

@ -4,7 +4,7 @@ import { Row, Card, CardHeader, CardBody, Button} from 'reactstrap';
import { Link } from "react-router-dom";
import { withNamespaces } from 'react-i18next';
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faArrowRight, faEllipsisV } from "@fortawesome/free-solid-svg-icons";
import { faArrowRight, faEllipsisV, faExclamationTriangle } from "@fortawesome/free-solid-svg-icons";
import ConfirmDialog from "../../../components/ConfirmDialog";
import {CURRENCY_DATA} from "../../../constants/currencies";
import {zeroAddress} from '../../../utils/address';
@ -77,7 +77,7 @@ class Offers extends Component {
{offer.arbitrator === zeroAddress && <dd>
<NoArbitratorWarning arbitrator={offer.arbitrator} />
</dd>}
{offer.arbitrator !== zeroAddress && <dd>{offer.arbitrator}</dd> }
{offer.arbitrator !== zeroAddress && <dd>{offer.arbitratorData.username} ({offer.arbitrator})</dd> }
</dl>
</Row>
</CardBody>

View File

@ -91,8 +91,10 @@ class MyProfile extends Component {
<Fragment>
<Trades trades={trades} address={this.props.address}/>
<Offers offers={profile.offers} location={profile.location} deleteOffer={this.props.deleteOffer} />
<Arbitrators />
{profile.isSeller && <Fragment>
<Offers offers={profile.offers} location={profile.location} deleteOffer={this.props.deleteOffer} />
<Arbitrators />
</Fragment>}
{profile.username && <StatusContactCode value={profile.statusContactCode} />}
</Fragment>
</Fragment>

View File

@ -30,7 +30,7 @@ class SelectArbitrator extends Component {
componentDidUpdate(prevProps) {
if ((!prevProps.arbitrators && this.props.arbitrators) || prevProps.arbitrators.length !== this.props.arbitrators.length || Object.keys(this.props.users).length !== this.props.arbitrators.length) {
this.props.arbitrators.forEach(arbitratorAddr => {
Object.keys(this.props.arbitrators).forEach(arbitratorAddr => {
if (!this.props.users[arbitratorAddr] && !this.loadedUsers.includes(arbitratorAddr)) {
this.props.getUser(arbitratorAddr);
this.loadedUsers.push(arbitratorAddr);
@ -86,7 +86,7 @@ class SelectArbitrator extends Component {
<Fragment>
<ArbitratorSelectorForm
value={this.state.selectedArbitrator}
arbitrators={this.props.arbitrators.filter(x => !addressCompare(x, this.props.address))}
arbitrators={Object.keys(this.props.arbitrators).filter(x => !addressCompare(x, this.props.address))}
changeArbitrator={this.changeArbitrator} users={this.props.users}
onSelectNoArbitrator={this.selectNoArbitrator} noArbitrator={this.state.noArbitrator}
/>