Convert ShapeShift modal to store (#4035)
* WIP * WIP store * Store in-place * WIP tests * Store completed * Expand option tests for events * Fix & test for errors found in manual testing * Add missing @observer (rookie mistake) * Fix intl formatting error (completed step) * Pass store to ErrorStep, test all stages for components * Add warning messages (e.g. no price found) * Fix typo
This commit is contained in:
parent
a076ffaf8c
commit
9613145464
@ -14,25 +14,21 @@
|
|||||||
// 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 { observer } from 'mobx-react';
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import Value from '../Value';
|
import Value from '../Value';
|
||||||
|
|
||||||
import styles from '../shapeshift.css';
|
import styles from '../shapeshift.css';
|
||||||
|
|
||||||
|
@observer
|
||||||
export default class AwaitingDepositStep extends Component {
|
export default class AwaitingDepositStep extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
coinSymbol: PropTypes.string.isRequired,
|
store: PropTypes.object.isRequired
|
||||||
depositAddress: PropTypes.string,
|
|
||||||
price: PropTypes.shape({
|
|
||||||
rate: PropTypes.number.isRequired,
|
|
||||||
minimum: PropTypes.number.isRequired,
|
|
||||||
limit: PropTypes.number.isRequired
|
|
||||||
}).isRequired
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { coinSymbol, depositAddress, price } = this.props;
|
const { coinSymbol, depositAddress, price } = this.props.store;
|
||||||
const typeSymbol = (
|
const typeSymbol = (
|
||||||
<div className={ styles.symbol }>
|
<div className={ styles.symbol }>
|
||||||
{ coinSymbol }
|
{ coinSymbol }
|
||||||
@ -43,22 +39,38 @@ export default class AwaitingDepositStep extends Component {
|
|||||||
return (
|
return (
|
||||||
<div className={ styles.center }>
|
<div className={ styles.center }>
|
||||||
<div className={ styles.busy }>
|
<div className={ styles.busy }>
|
||||||
Awaiting confirmation of the deposit address for your { typeSymbol } funds exchange
|
<FormattedMessage
|
||||||
|
id='shapeshift.awaitingDepositStep.awaitingConfirmation'
|
||||||
|
defaultMessage='Awaiting confirmation of the deposit address for your {typeSymbol} funds exchange'
|
||||||
|
values={ { typeSymbol } } />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ styles.center }>
|
<div className={ styles.center }>
|
||||||
<div className={ styles.info }>
|
<div className={ styles.info }>
|
||||||
<a href='https://shapeshift.io' target='_blank'>ShapeShift.io</a> is awaiting a { typeSymbol } deposit. Send the funds from your { typeSymbol } network client to -
|
<FormattedMessage
|
||||||
|
id='shapeshift.awaitingDepositStep.awaitingDeposit'
|
||||||
|
defaultMessage='{shapeshiftLink} is awaiting a {typeSymbol} deposit. Send the funds from your {typeSymbol} network client to -'
|
||||||
|
values={ {
|
||||||
|
shapeshiftLink: <a href='https://shapeshift.io' target='_blank'>ShapeShift.io</a>,
|
||||||
|
typeSymbol
|
||||||
|
} } />
|
||||||
</div>
|
</div>
|
||||||
<div className={ styles.hero }>
|
<div className={ styles.hero }>
|
||||||
{ depositAddress }
|
{ depositAddress }
|
||||||
</div>
|
</div>
|
||||||
<div className={ styles.price }>
|
<div className={ styles.price }>
|
||||||
<div>
|
<div>
|
||||||
(<Value amount={ price.minimum } symbol={ coinSymbol } /> minimum, <Value amount={ price.limit } symbol={ coinSymbol } /> maximum)
|
<FormattedMessage
|
||||||
|
id='shapeshift.awaitingDepositStep.minimumMaximum'
|
||||||
|
defaultMessage='{minimum} minimum, {maximum} maximum'
|
||||||
|
values={ {
|
||||||
|
maximum: <Value amount={ price.limit } symbol={ coinSymbol } />,
|
||||||
|
minimum: <Value amount={ price.minimum } symbol={ coinSymbol } />
|
||||||
|
} } />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1,50 @@
|
|||||||
|
// Copyright 2015, 2016 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 { shallow } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import AwaitingDepositStep from './';
|
||||||
|
|
||||||
|
let component;
|
||||||
|
|
||||||
|
function render () {
|
||||||
|
component = shallow(
|
||||||
|
<AwaitingDepositStep
|
||||||
|
store={ {
|
||||||
|
coinSymbol: 'BTC',
|
||||||
|
price: { rate: 0.001, minimum: 0, limit: 1.999 }
|
||||||
|
} } />
|
||||||
|
);
|
||||||
|
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('modals/Shapeshift/AwaitingDepositStep', () => {
|
||||||
|
it('renders defaults', () => {
|
||||||
|
expect(render()).to.be.ok;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays waiting for address with empty depositAddress', () => {
|
||||||
|
render();
|
||||||
|
expect(component.find('FormattedMessage').props().id).to.match(/awaitingConfirmation/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays waiting for deposit with non-empty depositAddress', () => {
|
||||||
|
render({ depositAddress: 'xyz' });
|
||||||
|
expect(component.find('FormattedMessage').first().props().id).to.match(/awaitingDeposit/);
|
||||||
|
});
|
||||||
|
});
|
@ -15,32 +15,39 @@
|
|||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
import Value from '../Value';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
|
||||||
|
import Value from '../Value';
|
||||||
import styles from '../shapeshift.css';
|
import styles from '../shapeshift.css';
|
||||||
|
|
||||||
|
@observer
|
||||||
export default class AwaitingExchangeStep extends Component {
|
export default class AwaitingExchangeStep extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
depositInfo: PropTypes.shape({
|
store: PropTypes.object.isRequired
|
||||||
incomingCoin: PropTypes.number.isRequired,
|
|
||||||
incomingType: PropTypes.string.isRequired
|
|
||||||
}).isRequired
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { depositInfo } = this.props;
|
const { depositInfo } = this.props.store;
|
||||||
const { incomingCoin, incomingType } = depositInfo;
|
const { incomingCoin, incomingType } = depositInfo;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ styles.center }>
|
<div className={ styles.center }>
|
||||||
<div className={ styles.info }>
|
<div className={ styles.info }>
|
||||||
<a href='https://shapeshift.io' target='_blank'>ShapeShift.io</a> has received a deposit of -
|
<FormattedMessage
|
||||||
|
id='shapeshift.awaitingExchangeStep.receivedInfo'
|
||||||
|
defaultMessage='{shapeshiftLink} has received a deposit of -'
|
||||||
|
values={ {
|
||||||
|
shapeshiftLink: <a href='https://shapeshift.io' target='_blank'>ShapeShift.io</a>
|
||||||
|
} } />
|
||||||
</div>
|
</div>
|
||||||
<div className={ styles.hero }>
|
<div className={ styles.hero }>
|
||||||
<Value amount={ incomingCoin } symbol={ incomingType } />
|
<Value amount={ incomingCoin } symbol={ incomingType } />
|
||||||
</div>
|
</div>
|
||||||
<div className={ styles.info }>
|
<div className={ styles.info }>
|
||||||
Awaiting the completion of the funds exchange and transfer of funds to your Parity account.
|
<FormattedMessage
|
||||||
|
id='shapeshift.awaitingExchangeStep.awaitingCompletion'
|
||||||
|
defaultMessage='Awaiting the completion of the funds exchange and transfer of funds to your Parity account.' />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
// Copyright 2015, 2016 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 { shallow } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import AwaitingExchangeStep from './';
|
||||||
|
|
||||||
|
let component;
|
||||||
|
|
||||||
|
function render () {
|
||||||
|
component = shallow(
|
||||||
|
<AwaitingExchangeStep
|
||||||
|
store={ {
|
||||||
|
depositInfo: { incomingCoin: 0.01, incomingType: 'BTC' }
|
||||||
|
} } />
|
||||||
|
);
|
||||||
|
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('modals/Shapeshift/AwaitingExchangeStep', () => {
|
||||||
|
it('renders defaults', () => {
|
||||||
|
expect(render()).to.be.ok;
|
||||||
|
});
|
||||||
|
});
|
@ -14,39 +14,41 @@
|
|||||||
// 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 { observer } from 'mobx-react';
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import Value from '../Value';
|
import Value from '../Value';
|
||||||
|
|
||||||
import styles from '../shapeshift.css';
|
import styles from '../shapeshift.css';
|
||||||
|
|
||||||
|
@observer
|
||||||
export default class CompletedStep extends Component {
|
export default class CompletedStep extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
depositInfo: PropTypes.shape({
|
store: PropTypes.object.isRequired
|
||||||
incomingCoin: PropTypes.number.isRequired,
|
|
||||||
incomingType: PropTypes.string.isRequired
|
|
||||||
}).isRequired,
|
|
||||||
exchangeInfo: PropTypes.shape({
|
|
||||||
outgoingCoin: PropTypes.string.isRequired,
|
|
||||||
outgoingType: PropTypes.string.isRequired
|
|
||||||
}).isRequired
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { depositInfo, exchangeInfo } = this.props;
|
const { depositInfo, exchangeInfo } = this.props.store;
|
||||||
const { incomingCoin, incomingType } = depositInfo;
|
const { incomingCoin, incomingType } = depositInfo;
|
||||||
const { outgoingCoin, outgoingType } = exchangeInfo;
|
const { outgoingCoin, outgoingType } = exchangeInfo;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ styles.center }>
|
<div className={ styles.center }>
|
||||||
<div className={ styles.info }>
|
<div className={ styles.info }>
|
||||||
<a href='https://shapeshift.io' target='_blank'>ShapeShift.io</a> has completed the funds exchange.
|
<FormattedMessage
|
||||||
|
id='shapeshift.completedStep.completed'
|
||||||
|
defaultMessage='{shapeshiftLink} has completed the funds exchange.'
|
||||||
|
values={ {
|
||||||
|
shapeshiftLink: <a href='https://shapeshift.io' target='_blank'>ShapeShift.io</a>
|
||||||
|
} } />
|
||||||
</div>
|
</div>
|
||||||
<div className={ styles.hero }>
|
<div className={ styles.hero }>
|
||||||
<Value amount={ incomingCoin } symbol={ incomingType } /> => <Value amount={ outgoingCoin } symbol={ outgoingType } />
|
<Value amount={ incomingCoin } symbol={ incomingType } /> => <Value amount={ outgoingCoin } symbol={ outgoingType } />
|
||||||
</div>
|
</div>
|
||||||
<div className={ styles.info }>
|
<div className={ styles.info }>
|
||||||
The change in funds will be reflected in your Parity account shortly.
|
<FormattedMessage
|
||||||
|
id='shapeshift.completedStep.parityFunds'
|
||||||
|
defaultMessage='The change in funds will be reflected in your Parity account shortly.' />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
40
js/src/modals/Shapeshift/CompletedStep/completedStep.spec.js
Normal file
40
js/src/modals/Shapeshift/CompletedStep/completedStep.spec.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// Copyright 2015, 2016 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 { shallow } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import CompletedStep from './';
|
||||||
|
|
||||||
|
let component;
|
||||||
|
|
||||||
|
function render () {
|
||||||
|
component = shallow(
|
||||||
|
<CompletedStep
|
||||||
|
store={ {
|
||||||
|
depositInfo: { incomingCoin: 0.01, incomingType: 'BTC' },
|
||||||
|
exchangeInfo: { outgoingCoin: 0.1, outgoingType: 'ETH' }
|
||||||
|
} } />
|
||||||
|
);
|
||||||
|
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('modals/Shapeshift/CompletedStep', () => {
|
||||||
|
it('renders defaults', () => {
|
||||||
|
expect(render()).to.be.ok;
|
||||||
|
});
|
||||||
|
});
|
@ -14,25 +14,30 @@
|
|||||||
// 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 { observer } from 'mobx-react';
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import styles from '../shapeshift.css';
|
import styles from '../shapeshift.css';
|
||||||
|
|
||||||
|
@observer
|
||||||
export default class ErrorStep extends Component {
|
export default class ErrorStep extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
error: PropTypes.shape({
|
store: PropTypes.object.isRequired
|
||||||
fatal: PropTypes.bool,
|
|
||||||
message: PropTypes.string.isRequired
|
|
||||||
}).isRequired
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { error } = this.props;
|
const { error } = this.props.store;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ styles.body }>
|
<div className={ styles.body }>
|
||||||
<div className={ styles.info }>
|
<div className={ styles.info }>
|
||||||
The funds shifting via <a href='https://shapeshift.io' target='_blank'>ShapeShift.io</a> failed with a fatal error on the exchange. The error message received from the exchange is as follow:
|
<FormattedMessage
|
||||||
|
id='shapeshift.errorStep.info'
|
||||||
|
defaultMessage='The funds shifting via {shapeshiftLink} failed with a fatal error on the exchange. The error message received from the exchange is as follow:'
|
||||||
|
values={ {
|
||||||
|
shapeshiftLink: <a href='https://shapeshift.io' target='_blank'>ShapeShift.io</a>
|
||||||
|
} } />
|
||||||
</div>
|
</div>
|
||||||
<div className={ styles.error }>
|
<div className={ styles.error }>
|
||||||
{ error.message }
|
{ error.message }
|
||||||
|
39
js/src/modals/Shapeshift/ErrorStep/errorStep.spec.js
Normal file
39
js/src/modals/Shapeshift/ErrorStep/errorStep.spec.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// Copyright 2015, 2016 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 { shallow } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import ErrorStep from './';
|
||||||
|
|
||||||
|
let component;
|
||||||
|
|
||||||
|
function render () {
|
||||||
|
component = shallow(
|
||||||
|
<ErrorStep
|
||||||
|
store={ {
|
||||||
|
error: new Error('testing')
|
||||||
|
} } />
|
||||||
|
);
|
||||||
|
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('modals/Shapeshift/ErrorStep', () => {
|
||||||
|
it('renders defaults', () => {
|
||||||
|
expect(render()).to.be.ok;
|
||||||
|
});
|
||||||
|
});
|
@ -14,64 +14,93 @@
|
|||||||
// 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 React, { Component, PropTypes } from 'react';
|
|
||||||
import { Checkbox, MenuItem } from 'material-ui';
|
import { Checkbox, MenuItem } from 'material-ui';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { Form, Input, Select } from '~/ui';
|
import { Form, Input, Select, Warning } from '~/ui';
|
||||||
|
|
||||||
import Price from '../Price';
|
import Price from '../Price';
|
||||||
|
import { WARNING_NO_PRICE } from '../store';
|
||||||
import styles from './optionsStep.css';
|
import styles from './optionsStep.css';
|
||||||
|
|
||||||
|
const WARNING_LABELS = {
|
||||||
|
[WARNING_NO_PRICE]: (
|
||||||
|
<FormattedMessage
|
||||||
|
id='shapeshift.warning.noPrice'
|
||||||
|
defaultMessage='No price match was found for the selected type' />
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
@observer
|
||||||
export default class OptionsStep extends Component {
|
export default class OptionsStep extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
refundAddress: PropTypes.string.isRequired,
|
store: PropTypes.object.isRequired
|
||||||
coinSymbol: PropTypes.string.isRequired,
|
|
||||||
coins: PropTypes.array.isRequired,
|
|
||||||
price: PropTypes.object,
|
|
||||||
hasAccepted: PropTypes.bool.isRequired,
|
|
||||||
onChangeSymbol: PropTypes.func.isRequired,
|
|
||||||
onChangeRefund: PropTypes.func.isRequired,
|
|
||||||
onToggleAccept: PropTypes.func.isRequired
|
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { coinSymbol, coins, refundAddress, hasAccepted, onToggleAccept } = this.props;
|
const { coinSymbol, coins, hasAcceptedTerms, price, refundAddress, warning } = this.props.store;
|
||||||
const label = `(optional) ${coinSymbol} return address`;
|
|
||||||
|
|
||||||
if (!coins.length) {
|
if (!coins.length) {
|
||||||
return (
|
return (
|
||||||
<div className={ styles.empty }>
|
<div className={ styles.empty }>
|
||||||
There are currently no exchange pairs/coins available to fund with.
|
<FormattedMessage
|
||||||
|
id='shapeshift.optionsStep.noPairs'
|
||||||
|
defaultMessage='There are currently no exchange pairs/coins available to fund with.' />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = coins.map(this.renderCoinSelectItem);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={ styles.body }>
|
<div className={ styles.body }>
|
||||||
<Form>
|
<Form>
|
||||||
<Select
|
<Select
|
||||||
className={ styles.coinselector }
|
className={ styles.coinselector }
|
||||||
label='fund account from'
|
hint={
|
||||||
hint='the type of crypto conversion to do'
|
<FormattedMessage
|
||||||
value={ coinSymbol }
|
id='shapeshift.optionsStep.typeSelect.hint'
|
||||||
onChange={ this.onSelectCoin }>
|
defaultMessage='the type of crypto conversion to do' />
|
||||||
{ items }
|
}
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='shapeshift.optionsStep.typeSelect.label'
|
||||||
|
defaultMessage='fund account from' />
|
||||||
|
}
|
||||||
|
onChange={ this.onSelectCoin }
|
||||||
|
value={ coinSymbol }>
|
||||||
|
{
|
||||||
|
coins.map(this.renderCoinSelectItem)
|
||||||
|
}
|
||||||
</Select>
|
</Select>
|
||||||
<Input
|
<Input
|
||||||
label={ label }
|
hint={
|
||||||
hint='the return address for send failures'
|
<FormattedMessage
|
||||||
value={ refundAddress }
|
id='shapeshift.optionsStep.returnAddr.hint'
|
||||||
onSubmit={ this.onChangeRefund } />
|
defaultMessage='the return address for send failures' />
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='shapeshift.optionsStep.returnAddr.label'
|
||||||
|
defaultMessage='(optional) {coinSymbol} return address'
|
||||||
|
values={ { coinSymbol } } />
|
||||||
|
}
|
||||||
|
onSubmit={ this.onChangeRefundAddress }
|
||||||
|
value={ refundAddress } />
|
||||||
<Checkbox
|
<Checkbox
|
||||||
|
checked={ hasAcceptedTerms }
|
||||||
className={ styles.accept }
|
className={ styles.accept }
|
||||||
label='I understand that ShapeShift.io is a 3rd-party service and by using the service any transfer of information and/or funds is completely out of the control of Parity'
|
label={
|
||||||
checked={ hasAccepted }
|
<FormattedMessage
|
||||||
onCheck={ onToggleAccept } />
|
id='shapeshift.optionsStep.terms.label'
|
||||||
|
defaultMessage='I understand that ShapeShift.io is a 3rd-party service and by using the service any transfer of information and/or funds is completely out of the control of Parity' />
|
||||||
|
}
|
||||||
|
onCheck={ this.onToggleAcceptTerms } />
|
||||||
</Form>
|
</Form>
|
||||||
<Price { ...this.props } />
|
<Warning warning={ WARNING_LABELS[warning] } />
|
||||||
|
<Price
|
||||||
|
coinSymbol={ coinSymbol }
|
||||||
|
price={ price } />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -81,7 +110,9 @@ export default class OptionsStep extends Component {
|
|||||||
|
|
||||||
const item = (
|
const item = (
|
||||||
<div className={ styles.coinselect }>
|
<div className={ styles.coinselect }>
|
||||||
<img className={ styles.coinimage } src={ image } />
|
<img
|
||||||
|
className={ styles.coinimage }
|
||||||
|
src={ image } />
|
||||||
<div className={ styles.coindetails }>
|
<div className={ styles.coindetails }>
|
||||||
<div className={ styles.coinsymbol }>
|
<div className={ styles.coinsymbol }>
|
||||||
{ symbol }
|
{ symbol }
|
||||||
@ -103,11 +134,15 @@ export default class OptionsStep extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
onSelectCoin = (event, idx, value) => {
|
onChangeRefundAddress = (event, refundAddress) => {
|
||||||
this.props.onChangeSymbol(event, value);
|
this.props.store.setRefundAddress(refundAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
onChangeAddress = (event, value) => {
|
onSelectCoin = (event, index, coinSymbol) => {
|
||||||
this.props.onChangeRefund(value);
|
this.props.store.setCoinSymbol(coinSymbol);
|
||||||
|
}
|
||||||
|
|
||||||
|
onToggleAcceptTerms = () => {
|
||||||
|
this.props.store.toggleAcceptTerms();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
126
js/src/modals/Shapeshift/OptionsStep/optionsSteps.spec.js
Normal file
126
js/src/modals/Shapeshift/OptionsStep/optionsSteps.spec.js
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
// Copyright 2015, 2016 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 { shallow } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
import Store, { WARNING_NO_PRICE } from '../store';
|
||||||
|
|
||||||
|
import OptionsStep from './';
|
||||||
|
|
||||||
|
const ADDRESS = '0x1234567890123456789012345678901234567890';
|
||||||
|
|
||||||
|
let component;
|
||||||
|
let instance;
|
||||||
|
let store;
|
||||||
|
|
||||||
|
function render () {
|
||||||
|
store = new Store(ADDRESS);
|
||||||
|
component = shallow(
|
||||||
|
<OptionsStep store={ store } />
|
||||||
|
);
|
||||||
|
instance = component.instance();
|
||||||
|
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('modals/Shapeshift/OptionsStep', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders defaults', () => {
|
||||||
|
expect(component).to.be.ok;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders no coins when none available', () => {
|
||||||
|
expect(component.find('FormattedMessage').props().id).to.equal('shapeshift.optionsStep.noPairs');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('components', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
store.setCoins([{ symbol: 'BTC', name: 'Bitcoin' }]);
|
||||||
|
store.toggleAcceptTerms();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('terms Checkbox', () => {
|
||||||
|
it('shows the state of store.hasAcceptedTerms', () => {
|
||||||
|
expect(component.find('Checkbox').props().checked).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('warning', () => {
|
||||||
|
let warning;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
store.setWarning(WARNING_NO_PRICE);
|
||||||
|
warning = component.find('Warning');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows a warning message when available', () => {
|
||||||
|
expect(warning.props().warning.props.id).to.equal('shapeshift.warning.noPrice');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('events', () => {
|
||||||
|
describe('onChangeRefundAddress', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.stub(store, 'setRefundAddress');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store.setRefundAddress.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the refundAddress on the store', () => {
|
||||||
|
instance.onChangeRefundAddress(null, 'refundAddress');
|
||||||
|
expect(store.setRefundAddress).to.have.been.calledWith('refundAddress');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onSelectCoin', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.stub(store, 'setCoinSymbol');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store.setCoinSymbol.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the coinSymbol on the store', () => {
|
||||||
|
instance.onSelectCoin(null, 0, 'XMR');
|
||||||
|
expect(store.setCoinSymbol).to.have.been.calledWith('XMR');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onToggleAcceptTerms', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.stub(store, 'toggleAcceptTerms');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store.toggleAcceptTerms.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('toggles the terms on the store', () => {
|
||||||
|
instance.onToggleAcceptTerms();
|
||||||
|
expect(store.toggleAcceptTerms).to.have.been.called;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -15,6 +15,7 @@
|
|||||||
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import Value from '../Value';
|
import Value from '../Value';
|
||||||
import styles from '../shapeshift.css';
|
import styles from '../shapeshift.css';
|
||||||
@ -42,7 +43,13 @@ export default class Price extends Component {
|
|||||||
<Value amount={ 1 } symbol={ coinSymbol } /> = <Value amount={ price.rate } />
|
<Value amount={ 1 } symbol={ coinSymbol } /> = <Value amount={ price.rate } />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
(<Value amount={ price.minimum } symbol={ coinSymbol } /> minimum, <Value amount={ price.limit } symbol={ coinSymbol } /> maximum)
|
<FormattedMessage
|
||||||
|
id='shapeshift.price.minMax'
|
||||||
|
defaultMessage='({minimum} minimum, {maximum} maximum)'
|
||||||
|
values={ {
|
||||||
|
maximum: <Value amount={ price.limit } symbol={ coinSymbol } />,
|
||||||
|
minimum: <Value amount={ price.minimum } symbol={ coinSymbol } />
|
||||||
|
} } />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
40
js/src/modals/Shapeshift/Price/price.spec.js
Normal file
40
js/src/modals/Shapeshift/Price/price.spec.js
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// Copyright 2015, 2016 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 { shallow } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import Price from './';
|
||||||
|
|
||||||
|
let component;
|
||||||
|
|
||||||
|
function render (props = {}) {
|
||||||
|
component = shallow(
|
||||||
|
<Price
|
||||||
|
coinSymbol='BTC'
|
||||||
|
price={ { rate: 0.1, minimum: 0.1, limit: 0.9 } }
|
||||||
|
error={ new Error('testing') }
|
||||||
|
{ ...props } />
|
||||||
|
);
|
||||||
|
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('modals/Shapeshift/Price', () => {
|
||||||
|
it('renders defaults', () => {
|
||||||
|
expect(render()).to.be.ok;
|
||||||
|
});
|
||||||
|
});
|
36
js/src/modals/Shapeshift/Value/value.spec.js
Normal file
36
js/src/modals/Shapeshift/Value/value.spec.js
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// Copyright 2015, 2016 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 { shallow } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import Value from './';
|
||||||
|
|
||||||
|
let component;
|
||||||
|
|
||||||
|
function render (props = {}) {
|
||||||
|
component = shallow(
|
||||||
|
<Value { ...props } />
|
||||||
|
);
|
||||||
|
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('modals/Shapeshift/Value', () => {
|
||||||
|
it('renders defaults', () => {
|
||||||
|
expect(render()).to.be.ok;
|
||||||
|
});
|
||||||
|
});
|
@ -14,26 +14,44 @@
|
|||||||
// 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 { observer } from 'mobx-react';
|
||||||
import React, { Component, PropTypes } from 'react';
|
import React, { Component, PropTypes } from 'react';
|
||||||
import ActionDoneAll from 'material-ui/svg-icons/action/done-all';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import ContentClear from 'material-ui/svg-icons/content/clear';
|
|
||||||
|
|
||||||
|
import shapeshiftLogo from '~/../assets/images/shapeshift-logo.png';
|
||||||
import { Button, IdentityIcon, Modal } from '~/ui';
|
import { Button, IdentityIcon, Modal } from '~/ui';
|
||||||
import initShapeshift from '~/3rdparty/shapeshift';
|
import { CancelIcon, DoneIcon } from '~/ui/Icons';
|
||||||
import shapeshiftLogo from '../../../assets/images/shapeshift-logo.png';
|
|
||||||
|
|
||||||
import AwaitingDepositStep from './AwaitingDepositStep';
|
import AwaitingDepositStep from './AwaitingDepositStep';
|
||||||
import AwaitingExchangeStep from './AwaitingExchangeStep';
|
import AwaitingExchangeStep from './AwaitingExchangeStep';
|
||||||
import CompletedStep from './CompletedStep';
|
import CompletedStep from './CompletedStep';
|
||||||
import ErrorStep from './ErrorStep';
|
import ErrorStep from './ErrorStep';
|
||||||
import OptionsStep from './OptionsStep';
|
import OptionsStep from './OptionsStep';
|
||||||
|
import Store, { STAGE_COMPLETED, STAGE_OPTIONS, STAGE_WAIT_DEPOSIT, STAGE_WAIT_EXCHANGE } from './store';
|
||||||
|
|
||||||
import styles from './shapeshift.css';
|
import styles from './shapeshift.css';
|
||||||
|
|
||||||
const shapeshift = initShapeshift();
|
const STAGE_TITLES = [
|
||||||
|
<FormattedMessage
|
||||||
const STAGE_NAMES = ['details', 'awaiting deposit', 'awaiting exchange', 'completed'];
|
id='shapeshift.title.details'
|
||||||
|
defaultMessage='details' />,
|
||||||
|
<FormattedMessage
|
||||||
|
id='shapeshift.title.deposit'
|
||||||
|
defaultMessage='awaiting deposit' />,
|
||||||
|
<FormattedMessage
|
||||||
|
id='shapeshift.title.exchange'
|
||||||
|
defaultMessage='awaiting exchange' />,
|
||||||
|
<FormattedMessage
|
||||||
|
id='shapeshift.title.completed'
|
||||||
|
defaultMessage='completed' />
|
||||||
|
];
|
||||||
|
const ERROR_TITLE = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='shapeshift.title.error'
|
||||||
|
defaultMessage='exchange failed' />
|
||||||
|
);
|
||||||
|
|
||||||
|
@observer
|
||||||
export default class Shapeshift extends Component {
|
export default class Shapeshift extends Component {
|
||||||
static contextTypes = {
|
static contextTypes = {
|
||||||
store: PropTypes.object.isRequired
|
store: PropTypes.object.isRequired
|
||||||
@ -44,46 +62,38 @@ export default class Shapeshift extends Component {
|
|||||||
onClose: PropTypes.func
|
onClose: PropTypes.func
|
||||||
}
|
}
|
||||||
|
|
||||||
state = {
|
store = new Store(this.props.address);
|
||||||
stage: 0,
|
|
||||||
coinSymbol: 'BTC',
|
|
||||||
coinPair: 'btc_eth',
|
|
||||||
coins: [],
|
|
||||||
depositAddress: '',
|
|
||||||
refundAddress: '',
|
|
||||||
price: null,
|
|
||||||
depositInfo: null,
|
|
||||||
exchangeInfo: null,
|
|
||||||
error: {},
|
|
||||||
hasAccepted: false,
|
|
||||||
shifting: false
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount () {
|
||||||
this.retrieveCoins();
|
this.store.retrieveCoins();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
this.unsubscribe();
|
this.store.unsubscribe();
|
||||||
}
|
|
||||||
|
|
||||||
unsubscribe () {
|
|
||||||
// Unsubscribe from Shapeshit
|
|
||||||
const { depositAddress } = this.state;
|
|
||||||
shapeshift.unsubscribe(depositAddress);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { error, stage } = this.state;
|
const { error, stage } = this.store;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
actions={ this.renderDialogActions() }
|
actions={ this.renderDialogActions() }
|
||||||
current={ stage }
|
current={ stage }
|
||||||
steps={ error.fatal ? null : STAGE_NAMES }
|
steps={
|
||||||
title={ error.fatal ? 'exchange failed' : null }
|
error
|
||||||
waiting={ [1, 2] }
|
? null
|
||||||
visible>
|
: STAGE_TITLES
|
||||||
|
}
|
||||||
|
title={
|
||||||
|
error
|
||||||
|
? ERROR_TITLE
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
visible
|
||||||
|
waiting={ [
|
||||||
|
STAGE_WAIT_DEPOSIT,
|
||||||
|
STAGE_WAIT_EXCHANGE
|
||||||
|
] }>
|
||||||
{ this.renderPage() }
|
{ this.renderPage() }
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
@ -91,7 +101,7 @@ export default class Shapeshift extends Component {
|
|||||||
|
|
||||||
renderDialogActions () {
|
renderDialogActions () {
|
||||||
const { address } = this.props;
|
const { address } = this.props;
|
||||||
const { coins, error, stage, hasAccepted, shifting } = this.state;
|
const { coins, error, hasAcceptedTerms, stage } = this.store;
|
||||||
|
|
||||||
const logo = (
|
const logo = (
|
||||||
<a href='http://shapeshift.io' target='_blank' className={ styles.shapeshift }>
|
<a href='http://shapeshift.io' target='_blank' className={ styles.shapeshift }>
|
||||||
@ -100,12 +110,16 @@ export default class Shapeshift extends Component {
|
|||||||
);
|
);
|
||||||
const cancelBtn = (
|
const cancelBtn = (
|
||||||
<Button
|
<Button
|
||||||
icon={ <ContentClear /> }
|
icon={ <CancelIcon /> }
|
||||||
label='Cancel'
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='shapeshift.button.cancel'
|
||||||
|
defaultMessage='Cancel' />
|
||||||
|
}
|
||||||
onClick={ this.onClose } />
|
onClick={ this.onClose } />
|
||||||
);
|
);
|
||||||
|
|
||||||
if (error.fatal) {
|
if (error) {
|
||||||
return [
|
return [
|
||||||
logo,
|
logo,
|
||||||
cancelBtn
|
cancelBtn
|
||||||
@ -113,208 +127,85 @@ export default class Shapeshift extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (stage) {
|
switch (stage) {
|
||||||
case 0:
|
case STAGE_OPTIONS:
|
||||||
return [
|
return [
|
||||||
logo,
|
logo,
|
||||||
cancelBtn,
|
cancelBtn,
|
||||||
<Button
|
<Button
|
||||||
disabled={ !coins.length || !hasAccepted || shifting }
|
disabled={ !coins.length || !hasAcceptedTerms }
|
||||||
icon={ <IdentityIcon address={ address } button /> }
|
icon={
|
||||||
label='Shift Funds'
|
<IdentityIcon
|
||||||
|
address={ address }
|
||||||
|
button />
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='shapeshift.button.shift'
|
||||||
|
defaultMessage='Shift Funds' />
|
||||||
|
}
|
||||||
onClick={ this.onShift } />
|
onClick={ this.onShift } />
|
||||||
];
|
];
|
||||||
|
|
||||||
case 1:
|
case STAGE_WAIT_DEPOSIT:
|
||||||
case 2:
|
case STAGE_WAIT_EXCHANGE:
|
||||||
return [
|
return [
|
||||||
logo,
|
logo,
|
||||||
cancelBtn
|
cancelBtn
|
||||||
];
|
];
|
||||||
|
|
||||||
case 3:
|
case STAGE_COMPLETED:
|
||||||
return [
|
return [
|
||||||
logo,
|
logo,
|
||||||
<Button
|
<Button
|
||||||
icon={ <ActionDoneAll /> }
|
icon={ <DoneIcon /> }
|
||||||
label='Close'
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='shapeshift.button.done'
|
||||||
|
defaultMessage='Close' />
|
||||||
|
}
|
||||||
onClick={ this.onClose } />
|
onClick={ this.onClose } />
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
renderPage () {
|
renderPage () {
|
||||||
const { error, stage } = this.state;
|
const { error, stage } = this.store;
|
||||||
|
|
||||||
if (error.fatal) {
|
if (error) {
|
||||||
return (
|
return (
|
||||||
<ErrorStep error={ error } />
|
<ErrorStep store={ this.store } />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (stage) {
|
switch (stage) {
|
||||||
case 0:
|
case STAGE_OPTIONS:
|
||||||
return (
|
return (
|
||||||
<OptionsStep
|
<OptionsStep store={ this.store } />
|
||||||
{ ...this.state }
|
|
||||||
onChangeSymbol={ this.onChangeSymbol }
|
|
||||||
onChangeRefund={ this.onChangeRefund }
|
|
||||||
onToggleAccept={ this.onToggleAccept } />
|
|
||||||
);
|
);
|
||||||
|
|
||||||
case 1:
|
case STAGE_WAIT_DEPOSIT:
|
||||||
return (
|
return (
|
||||||
<AwaitingDepositStep { ...this.state } />
|
<AwaitingDepositStep store={ this.store } />
|
||||||
);
|
);
|
||||||
|
|
||||||
case 2:
|
case STAGE_WAIT_EXCHANGE:
|
||||||
return (
|
return (
|
||||||
<AwaitingExchangeStep { ...this.state } />
|
<AwaitingExchangeStep store={ this.store } />
|
||||||
);
|
);
|
||||||
|
|
||||||
case 3:
|
case STAGE_COMPLETED:
|
||||||
return (
|
return (
|
||||||
<CompletedStep { ...this.state } />
|
<CompletedStep store={ this.store } />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setStage (stage) {
|
|
||||||
this.setState({
|
|
||||||
stage,
|
|
||||||
error: {}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setFatalError (message) {
|
|
||||||
this.setState({
|
|
||||||
stage: 0,
|
|
||||||
error: {
|
|
||||||
fatal: true,
|
|
||||||
message
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onClose = () => {
|
onClose = () => {
|
||||||
this.setStage(0);
|
this.store.setStage(STAGE_OPTIONS);
|
||||||
this.props.onClose && this.props.onClose();
|
this.props.onClose && this.props.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
onShift = () => {
|
onShift = () => {
|
||||||
const { address } = this.props;
|
return this.store.shift();
|
||||||
const { coinPair, refundAddress } = this.state;
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
stage: 1,
|
|
||||||
shifting: true
|
|
||||||
});
|
|
||||||
|
|
||||||
shapeshift
|
|
||||||
.shift(address, refundAddress, coinPair)
|
|
||||||
.then((result) => {
|
|
||||||
console.log('onShift', result);
|
|
||||||
const depositAddress = result.deposit;
|
|
||||||
|
|
||||||
if (this.state.depositAddress) {
|
|
||||||
this.unsubscribe();
|
|
||||||
}
|
|
||||||
|
|
||||||
shapeshift.subscribe(depositAddress, this.onExchangeInfo);
|
|
||||||
this.setState({ depositAddress });
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('onShift', error);
|
|
||||||
const message = `Failed to start exchange: ${error.message}`;
|
|
||||||
|
|
||||||
this.newError(new Error(message));
|
|
||||||
this.setFatalError(message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeSymbol = (event, coinSymbol) => {
|
|
||||||
const coinPair = `${coinSymbol.toLowerCase()}_eth`;
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
coinPair,
|
|
||||||
coinSymbol,
|
|
||||||
price: null
|
|
||||||
});
|
|
||||||
this.getPrice(coinPair);
|
|
||||||
}
|
|
||||||
|
|
||||||
onChangeRefund = (event, refundAddress) => {
|
|
||||||
this.setState({ refundAddress });
|
|
||||||
}
|
|
||||||
|
|
||||||
onToggleAccept = () => {
|
|
||||||
const { hasAccepted } = this.state;
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
hasAccepted: !hasAccepted
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onExchangeInfo = (error, result) => {
|
|
||||||
if (error) {
|
|
||||||
console.error('onExchangeInfo', error);
|
|
||||||
|
|
||||||
if (error.fatal) {
|
|
||||||
this.setFatalError(error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.newError(error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('onExchangeInfo', result.status, result);
|
|
||||||
|
|
||||||
switch (result.status) {
|
|
||||||
case 'received':
|
|
||||||
this.setState({ depositInfo: result });
|
|
||||||
this.setStage(2);
|
|
||||||
return;
|
|
||||||
|
|
||||||
case 'complete':
|
|
||||||
this.setState({ exchangeInfo: result });
|
|
||||||
this.setStage(3);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getPrice (coinPair) {
|
|
||||||
shapeshift
|
|
||||||
.getMarketInfo(coinPair)
|
|
||||||
.then((price) => {
|
|
||||||
this.setState({ price });
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('getPrice', error);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
retrieveCoins () {
|
|
||||||
const { coinPair } = this.state;
|
|
||||||
|
|
||||||
shapeshift
|
|
||||||
.getCoins()
|
|
||||||
.then((_coins) => {
|
|
||||||
const coins = Object.values(_coins).filter((coin) => coin.status === 'available');
|
|
||||||
|
|
||||||
this.getPrice(coinPair);
|
|
||||||
this.setState({ coins });
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('retrieveCoins', error);
|
|
||||||
const message = `Failed to retrieve available coins from ShapeShift.io: ${error.message}`;
|
|
||||||
|
|
||||||
this.newError(new Error(message));
|
|
||||||
this.setFatalError(message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
newError (error) {
|
|
||||||
const { store } = this.context;
|
|
||||||
|
|
||||||
store.dispatch({ type: 'newError', error });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
159
js/src/modals/Shapeshift/shapeshift.spec.js
Normal file
159
js/src/modals/Shapeshift/shapeshift.spec.js
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
// Copyright 2015, 2016 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 { shallow } from 'enzyme';
|
||||||
|
import React from 'react';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
import { STAGE_COMPLETED, STAGE_OPTIONS, STAGE_WAIT_DEPOSIT, STAGE_WAIT_EXCHANGE } from './store';
|
||||||
|
import Shapeshift from './';
|
||||||
|
|
||||||
|
const ADDRESS = '0x0123456789012345678901234567890123456789';
|
||||||
|
|
||||||
|
let component;
|
||||||
|
let instance;
|
||||||
|
let onClose;
|
||||||
|
|
||||||
|
function render (props = {}) {
|
||||||
|
onClose = sinon.stub();
|
||||||
|
component = shallow(
|
||||||
|
<Shapeshift
|
||||||
|
address={ ADDRESS }
|
||||||
|
onClose={ onClose }
|
||||||
|
{ ...props } />,
|
||||||
|
{ context: { store: {} } }
|
||||||
|
);
|
||||||
|
instance = component.instance();
|
||||||
|
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('modals/Shapeshift', () => {
|
||||||
|
it('renders defaults', () => {
|
||||||
|
expect(render()).to.be.ok;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('componentDidMount', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
render();
|
||||||
|
sinon.stub(instance.store, 'retrieveCoins');
|
||||||
|
return instance.componentDidMount();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
instance.store.retrieveCoins.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('retrieves the list of coins when mounting', () => {
|
||||||
|
expect(instance.store.retrieveCoins).to.have.been.called;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('componentWillUnmount', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
render();
|
||||||
|
sinon.stub(instance.store, 'unsubscribe');
|
||||||
|
return instance.componentWillUnmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
instance.store.unsubscribe.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes any subscriptions when unmounting', () => {
|
||||||
|
expect(instance.store.unsubscribe).to.have.been.called;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('renderDialogActions', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('shift button', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.stub(instance.store, 'shift').resolves();
|
||||||
|
|
||||||
|
instance.store.setCoins(['BTC']);
|
||||||
|
instance.store.toggleAcceptTerms();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
instance.store.shift.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('disabled shift button when not accepted', () => {
|
||||||
|
instance.store.toggleAcceptTerms();
|
||||||
|
expect(shallow(instance.renderDialogActions()[2]).props().disabled).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows shift button when accepted', () => {
|
||||||
|
expect(shallow(instance.renderDialogActions()[2]).props().disabled).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls the shift on store when clicked', () => {
|
||||||
|
shallow(instance.renderDialogActions()[2]).simulate('touchTap');
|
||||||
|
expect(instance.store.shift).to.have.been.called;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('renderPage', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders ErrorStep on error, passing the store', () => {
|
||||||
|
instance.store.setError('testError');
|
||||||
|
const page = instance.renderPage();
|
||||||
|
|
||||||
|
expect(page.type).to.match(/ErrorStep/);
|
||||||
|
expect(page.props.store).to.equal(instance.store);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders OptionsStep with STAGE_OPTIONS, passing the store', () => {
|
||||||
|
instance.store.setStage(STAGE_OPTIONS);
|
||||||
|
const page = instance.renderPage();
|
||||||
|
|
||||||
|
expect(page.type).to.match(/OptionsStep/);
|
||||||
|
expect(page.props.store).to.equal(instance.store);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders AwaitingDepositStep with STAGE_WAIT_DEPOSIT, passing the store', () => {
|
||||||
|
instance.store.setStage(STAGE_WAIT_DEPOSIT);
|
||||||
|
const page = instance.renderPage();
|
||||||
|
|
||||||
|
expect(page.type).to.match(/AwaitingDepositStep/);
|
||||||
|
expect(page.props.store).to.equal(instance.store);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders AwaitingExchangeStep with STAGE_WAIT_EXCHANGE, passing the store', () => {
|
||||||
|
instance.store.setStage(STAGE_WAIT_EXCHANGE);
|
||||||
|
const page = instance.renderPage();
|
||||||
|
|
||||||
|
expect(page.type).to.match(/AwaitingExchangeStep/);
|
||||||
|
expect(page.props.store).to.equal(instance.store);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders CompletedStep with STAGE_COMPLETED, passing the store', () => {
|
||||||
|
instance.store.setStage(STAGE_COMPLETED);
|
||||||
|
const page = instance.renderPage();
|
||||||
|
|
||||||
|
expect(page.type).to.match(/CompletedStep/);
|
||||||
|
expect(page.props.store).to.equal(instance.store);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
199
js/src/modals/Shapeshift/store.js
Normal file
199
js/src/modals/Shapeshift/store.js
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
// Copyright 2015, 2016 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, observable, transaction } from 'mobx';
|
||||||
|
|
||||||
|
import initShapeshift from '~/3rdparty/shapeshift';
|
||||||
|
|
||||||
|
const STAGE_OPTIONS = 0;
|
||||||
|
const STAGE_WAIT_DEPOSIT = 1;
|
||||||
|
const STAGE_WAIT_EXCHANGE = 2;
|
||||||
|
const STAGE_COMPLETED = 3;
|
||||||
|
|
||||||
|
const WARNING_NONE = 0;
|
||||||
|
const WARNING_NO_PRICE = -1;
|
||||||
|
|
||||||
|
export default class Store {
|
||||||
|
@observable address = null;
|
||||||
|
@observable coinPair = 'btc_eth';
|
||||||
|
@observable coinSymbol = 'BTC';
|
||||||
|
@observable coins = [];
|
||||||
|
@observable depositAddress = '';
|
||||||
|
@observable depositInfo = null;
|
||||||
|
@observable exchangeInfo = null;
|
||||||
|
@observable error = null;
|
||||||
|
@observable hasAcceptedTerms = false;
|
||||||
|
@observable price = null;
|
||||||
|
@observable refundAddress = '';
|
||||||
|
@observable stage = STAGE_OPTIONS;
|
||||||
|
@observable warning = 0;
|
||||||
|
|
||||||
|
constructor (address) {
|
||||||
|
this._shapeshiftApi = initShapeshift();
|
||||||
|
this.address = address;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setCoins = (coins) => {
|
||||||
|
this.coins = coins;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setCoinSymbol = (coinSymbol) => {
|
||||||
|
transaction(() => {
|
||||||
|
this.coinSymbol = coinSymbol;
|
||||||
|
this.coinPair = `${coinSymbol.toLowerCase()}_eth`;
|
||||||
|
this.price = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.getCoinPrice();
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setDepositAddress = (depositAddress) => {
|
||||||
|
this.depositAddress = depositAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setDepositInfo = (depositInfo) => {
|
||||||
|
transaction(() => {
|
||||||
|
this.depositInfo = depositInfo;
|
||||||
|
this.setStage(STAGE_WAIT_EXCHANGE);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setError = (error) => {
|
||||||
|
this.error = error;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setExchangeInfo = (exchangeInfo) => {
|
||||||
|
transaction(() => {
|
||||||
|
this.exchangeInfo = exchangeInfo;
|
||||||
|
this.setStage(STAGE_COMPLETED);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setPrice = (price) => {
|
||||||
|
transaction(() => {
|
||||||
|
this.price = price;
|
||||||
|
this.setWarning();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setRefundAddress = (refundAddress) => {
|
||||||
|
this.refundAddress = refundAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setStage = (stage) => {
|
||||||
|
this.stage = stage;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action setWarning = (warning = WARNING_NONE) => {
|
||||||
|
this.warning = warning;
|
||||||
|
}
|
||||||
|
|
||||||
|
@action toggleAcceptTerms = () => {
|
||||||
|
this.hasAcceptedTerms = !this.hasAcceptedTerms;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCoinPrice () {
|
||||||
|
return this._shapeshiftApi
|
||||||
|
.getMarketInfo(this.coinPair)
|
||||||
|
.then((price) => {
|
||||||
|
this.setPrice(price);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.warn('getCoinPrice', error);
|
||||||
|
|
||||||
|
this.setWarning(WARNING_NO_PRICE);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
retrieveCoins () {
|
||||||
|
return this._shapeshiftApi
|
||||||
|
.getCoins()
|
||||||
|
.then((coins) => {
|
||||||
|
this.setCoins(Object.values(coins).filter((coin) => coin.status === 'available'));
|
||||||
|
|
||||||
|
return this.getCoinPrice();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('retrieveCoins', error);
|
||||||
|
const message = `Failed to retrieve available coins from ShapeShift.io: ${error.message}`;
|
||||||
|
|
||||||
|
this.setError(message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
shift () {
|
||||||
|
this.setStage(STAGE_WAIT_DEPOSIT);
|
||||||
|
|
||||||
|
return this._shapeshiftApi
|
||||||
|
.shift(this.address, this.refundAddress, this.coinPair)
|
||||||
|
.then((result) => {
|
||||||
|
console.log('onShift', result);
|
||||||
|
|
||||||
|
this.setDepositAddress(result.deposit);
|
||||||
|
return this.subscribe();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('onShift', error);
|
||||||
|
const message = `Failed to start exchange: ${error.message}`;
|
||||||
|
|
||||||
|
this.setError(new Error(message));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onExchangeInfo = (error, result) => {
|
||||||
|
if (error) {
|
||||||
|
console.error('onExchangeInfo', error);
|
||||||
|
|
||||||
|
if (error.fatal) {
|
||||||
|
this.setError(error);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('onExchangeInfo', result.status, result);
|
||||||
|
|
||||||
|
switch (result.status) {
|
||||||
|
case 'received':
|
||||||
|
if (this.stage !== STAGE_WAIT_EXCHANGE) {
|
||||||
|
this.setDepositInfo(result);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
|
||||||
|
case 'complete':
|
||||||
|
if (this.stage !== STAGE_COMPLETED) {
|
||||||
|
this.setExchangeInfo(result);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe () {
|
||||||
|
return this._shapeshiftApi.subscribe(this.depositAddress, this.onExchangeInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
unsubscribe () {
|
||||||
|
return this._shapeshiftApi.unsubscribe(this.depositAddress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
STAGE_COMPLETED,
|
||||||
|
STAGE_OPTIONS,
|
||||||
|
STAGE_WAIT_DEPOSIT,
|
||||||
|
STAGE_WAIT_EXCHANGE,
|
||||||
|
WARNING_NONE,
|
||||||
|
WARNING_NO_PRICE
|
||||||
|
};
|
355
js/src/modals/Shapeshift/store.spec.js
Normal file
355
js/src/modals/Shapeshift/store.spec.js
Normal file
@ -0,0 +1,355 @@
|
|||||||
|
// Copyright 2015, 2016 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, { STAGE_COMPLETED, STAGE_OPTIONS, STAGE_WAIT_DEPOSIT, STAGE_WAIT_EXCHANGE, WARNING_NONE, WARNING_NO_PRICE } from './store';
|
||||||
|
|
||||||
|
const ADDRESS = '0xabcdeffdecbaabcdeffdecbaabcdeffdecbaabcdeffdecba';
|
||||||
|
|
||||||
|
describe('modals/Shapeshift/Store', () => {
|
||||||
|
let store;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
store = new Store(ADDRESS);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stores the ETH address', () => {
|
||||||
|
expect(store.address).to.equal(ADDRESS);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defaults to BTC-ETH pair', () => {
|
||||||
|
expect(store.coinSymbol).to.equal('BTC');
|
||||||
|
expect(store.coinPair).to.equal('btc_eth');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defaults to stage STAGE_OPTIONS', () => {
|
||||||
|
expect(store.stage).to.equal(STAGE_OPTIONS);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defaults to terms not accepted', () => {
|
||||||
|
expect(store.hasAcceptedTerms).to.be.false;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('@action', () => {
|
||||||
|
describe('setCoins', () => {
|
||||||
|
it('sets the available coins', () => {
|
||||||
|
const coins = ['BTC', 'ETC', 'XMR'];
|
||||||
|
|
||||||
|
store.setCoins(coins);
|
||||||
|
expect(store.coins.peek()).to.deep.equal(coins);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setCoinSymbol', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.stub(store, 'getCoinPrice');
|
||||||
|
store.setCoinSymbol('XMR');
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store.getCoinPrice.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the coinSymbol', () => {
|
||||||
|
expect(store.coinSymbol).to.equal('XMR');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the coinPair', () => {
|
||||||
|
expect(store.coinPair).to.equal('xmr_eth');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resets the price retrieved', () => {
|
||||||
|
expect(store.price).to.be.null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('retrieves the pair price', () => {
|
||||||
|
expect(store.getCoinPrice).to.have.been.called;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setDepositAddress', () => {
|
||||||
|
it('sets the depositAddress', () => {
|
||||||
|
store.setDepositAddress('testing');
|
||||||
|
expect(store.depositAddress).to.equal('testing');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setDepositInfo', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
store.setDepositInfo('testing');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the depositInfo', () => {
|
||||||
|
expect(store.depositInfo).to.equal('testing');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the stage to STAGE_WAIT_EXCHANGE', () => {
|
||||||
|
expect(store.stage).to.equal(STAGE_WAIT_EXCHANGE);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setError', () => {
|
||||||
|
it('sets the error', () => {
|
||||||
|
store.setError(new Error('testing'));
|
||||||
|
expect(store.error).to.match(/testing/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setExchangeInfo', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
store.setExchangeInfo('testing');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the exchangeInfo', () => {
|
||||||
|
expect(store.exchangeInfo).to.equal('testing');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the stage to STAGE_COMPLETED', () => {
|
||||||
|
expect(store.stage).to.equal(STAGE_COMPLETED);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setPrice', () => {
|
||||||
|
it('sets the price', () => {
|
||||||
|
store.setPrice('testing');
|
||||||
|
expect(store.price).to.equal('testing');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clears any warnings once set', () => {
|
||||||
|
store.setWarning(-999);
|
||||||
|
store.setPrice('testing');
|
||||||
|
expect(store.warning).to.equal(WARNING_NONE);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setRefundAddress', () => {
|
||||||
|
it('sets the price', () => {
|
||||||
|
store.setRefundAddress('testing');
|
||||||
|
expect(store.refundAddress).to.equal('testing');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setStage', () => {
|
||||||
|
it('sets the state', () => {
|
||||||
|
store.setStage('testing');
|
||||||
|
expect(store.stage).to.equal('testing');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setWarning', () => {
|
||||||
|
it('sets the warning', () => {
|
||||||
|
store.setWarning(-999);
|
||||||
|
|
||||||
|
expect(store.warning).to.equal(-999);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clears the warning with no parameters', () => {
|
||||||
|
store.setWarning(-999);
|
||||||
|
store.setWarning();
|
||||||
|
|
||||||
|
expect(store.warning).to.equal(WARNING_NONE);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toggleAcceptTerms', () => {
|
||||||
|
it('changes state on hasAcceptedTerms', () => {
|
||||||
|
store.toggleAcceptTerms();
|
||||||
|
expect(store.hasAcceptedTerms).to.be.true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('operations', () => {
|
||||||
|
describe('getCoinPrice', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.stub(store._shapeshiftApi, 'getMarketInfo').resolves('testPrice');
|
||||||
|
return store.getCoinPrice();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store._shapeshiftApi.getMarketInfo.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('retrieves the market info from ShapeShift', () => {
|
||||||
|
expect(store._shapeshiftApi.getMarketInfo).to.have.been.calledWith('btc_eth');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stores the price retrieved', () => {
|
||||||
|
expect(store.price).to.equal('testPrice');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets a warning on failure', () => {
|
||||||
|
store._shapeshiftApi.getMarketInfo.restore();
|
||||||
|
sinon.stub(store._shapeshiftApi, 'getMarketInfo').rejects('someError');
|
||||||
|
|
||||||
|
return store.getCoinPrice().then(() => {
|
||||||
|
expect(store.warning).to.equal(WARNING_NO_PRICE);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('retrieveCoins', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.stub(store._shapeshiftApi, 'getCoins').resolves({
|
||||||
|
BTC: { symbol: 'BTC', status: 'available' },
|
||||||
|
ETC: { symbol: 'ETC' },
|
||||||
|
XMR: { symbol: 'XMR', status: 'available' }
|
||||||
|
});
|
||||||
|
sinon.stub(store, 'getCoinPrice');
|
||||||
|
return store.retrieveCoins();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store._shapeshiftApi.getCoins.restore();
|
||||||
|
store.getCoinPrice.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('retrieves the coins from ShapeShift', () => {
|
||||||
|
expect(store._shapeshiftApi.getCoins).to.have.been.called;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the available coins', () => {
|
||||||
|
expect(store.coins.peek()).to.deep.equal([
|
||||||
|
{ status: 'available', symbol: 'BTC' },
|
||||||
|
{ status: 'available', symbol: 'XMR' }
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('retrieves the price once resolved', () => {
|
||||||
|
expect(store.getCoinPrice).to.have.been.called;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('shift', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.stub(store, 'subscribe').resolves();
|
||||||
|
sinon.stub(store._shapeshiftApi, 'shift').resolves({ deposit: 'depositAddress' });
|
||||||
|
store.setRefundAddress('refundAddress');
|
||||||
|
|
||||||
|
return store.shift();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store.subscribe.restore();
|
||||||
|
store._shapeshiftApi.shift.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('moves to stage STAGE_WAIT_DEPOSIT', () => {
|
||||||
|
expect(store.stage).to.equal(STAGE_WAIT_DEPOSIT);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls ShapeShift with the correct parameters', () => {
|
||||||
|
expect(store._shapeshiftApi.shift).to.have.been.calledWith(ADDRESS, 'refundAddress', store.coinPair);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the depositAddress', () => {
|
||||||
|
expect(store.depositAddress).to.equal('depositAddress');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('subscribes to updates', () => {
|
||||||
|
expect(store.subscribe).to.have.been.called;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets error when shift fails', () => {
|
||||||
|
store._shapeshiftApi.shift.restore();
|
||||||
|
sinon.stub(store._shapeshiftApi, 'shift').rejects({ message: 'testingError' });
|
||||||
|
|
||||||
|
return store.shift().then(() => {
|
||||||
|
expect(store.error).to.match(/testingError/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('subscribe', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.stub(store._shapeshiftApi, 'subscribe');
|
||||||
|
store.setDepositAddress('depositAddress');
|
||||||
|
return store.subscribe();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store._shapeshiftApi.subscribe.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls into the ShapeShift subscribe', () => {
|
||||||
|
expect(store._shapeshiftApi.subscribe).to.have.been.calledWith('depositAddress', store.onExchangeInfo);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onExchangeInfo', () => {
|
||||||
|
it('sets the error when fatal error retrieved', () => {
|
||||||
|
store.onExchangeInfo({ fatal: true, message: 'testing' });
|
||||||
|
expect(store.error.message).to.equal('testing');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not set the error when non-fatal error retrieved', () => {
|
||||||
|
store.onExchangeInfo({ message: 'testing' });
|
||||||
|
expect(store.error).to.be.null;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('status received', () => {
|
||||||
|
const INFO = { status: 'received' };
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
store.onExchangeInfo(null, INFO);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the depositInfo', () => {
|
||||||
|
expect(store.depositInfo).to.deep.equal(INFO);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('only advanced depositInfo once', () => {
|
||||||
|
store.onExchangeInfo(null, Object.assign({}, INFO, { state: 'secondTime' }));
|
||||||
|
expect(store.depositInfo).to.deep.equal(INFO);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('status completed', () => {
|
||||||
|
const INFO = { status: 'complete' };
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
store.onExchangeInfo(null, INFO);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sets the depositInfo', () => {
|
||||||
|
expect(store.exchangeInfo).to.deep.equal(INFO);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('only advanced depositInfo once', () => {
|
||||||
|
store.onExchangeInfo(null, Object.assign({}, INFO, { state: 'secondTime' }));
|
||||||
|
expect(store.exchangeInfo).to.deep.equal(INFO);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('unsubscribe', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
sinon.stub(store._shapeshiftApi, 'unsubscribe');
|
||||||
|
store.setDepositAddress('depositAddress');
|
||||||
|
return store.unsubscribe();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
store._shapeshiftApi.unsubscribe.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls into the ShapeShift unsubscribe', () => {
|
||||||
|
expect(store._shapeshiftApi.unsubscribe).to.have.been.calledWith('depositAddress');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user