From 5cbbb476b3eb7a5fd70b014b2a1a83fea7092b58 Mon Sep 17 00:00:00 2001 From: Chi Kei Chan Date: Mon, 25 Sep 2017 14:51:49 -0700 Subject: [PATCH] ShapeShift Integration --- .../src/app/first-time/buy-ether-screen.js | 83 +------ mascara/src/app/first-time/index.css | 31 ++- mascara/src/app/shapeshift-form/index.js | 217 ++++++++++++++++++ ui/app/actions.js | 24 +- ui/app/reducers/app.js | 1 - ui/app/reducers/metamask.js | 21 ++ 6 files changed, 287 insertions(+), 90 deletions(-) create mode 100644 mascara/src/app/shapeshift-form/index.js diff --git a/mascara/src/app/first-time/buy-ether-screen.js b/mascara/src/app/first-time/buy-ether-screen.js index 44141db64..45b2df1c8 100644 --- a/mascara/src/app/first-time/buy-ether-screen.js +++ b/mascara/src/app/first-time/buy-ether-screen.js @@ -3,6 +3,7 @@ import classnames from 'classnames' import {connect} from 'react-redux' import {qrcode} from 'qrcode-npm' import copyToClipboard from 'copy-to-clipboard' +import ShapeShiftForm from '../shapeshift-form' import Identicon from '../../../../ui/app/components/identicon' import {buyEth, showAccountDetail} from '../../../../ui/app/actions' @@ -79,12 +80,6 @@ class BuyEtherScreen extends Component { ) } - renderShapeShiftLogo () { - return ( -
- ) - } - renderCoinbaseForm () { const {goToCoinbase, address} = this.props @@ -119,83 +114,13 @@ class BuyEtherScreen extends Component { case OPTION_VALUES.SHAPESHIFT: return (
-
{this.renderShapeShiftLogo()}
+
Trade any leading blockchain asset for any other. Protection by Design. No Account Needed.
-
-
-
-
- Deposit -
- -
-
-
-
- Receive -
- -
-
-
-
- Your Refund Address -
- -
-
-
-
- Status: -
-
- Available -
-
-
-
- Limit: -
-
- 2.06856464 -
-
-
-
- Exchange Rate: -
-
- 12.21840214 -
-
-
-
- Minimum: -
-
- 0.000163 -
-
-
-
-
- -
+
- ) + ) case OPTION_VALUES.QR_CODE: return (
diff --git a/mascara/src/app/first-time/index.css b/mascara/src/app/first-time/index.css index 50d0d2fb7..c9c3f6380 100644 --- a/mascara/src/app/first-time/index.css +++ b/mascara/src/app/first-time/index.css @@ -578,7 +578,8 @@ button.first-time-flow__button--tertiary:hover { flex: 1 0 auto; } -.shapeshift-form__selector-label { +.shapeshift-form__selector-label, +.shapeshift-form__deposit-instruction { color: #757575; color: rgba(0, 0, 0, 0.45); font-family: Montserrat Light; @@ -597,10 +598,8 @@ button.first-time-flow__button--tertiary:hover { text-align: center; width: 100%; height: 45px; -} - -.shapeshift-form__address-input-wrapper { - padding-bottom: 24px; + line-height: 44px; + font-family: Montserrat Light; } .shapeshift-form__address-input-label { @@ -622,6 +621,18 @@ button.first-time-flow__button--tertiary:hover { width: 100%; } +.shapeshift-form__address-input-wrapper--error .shapeshift-form__address-input { + border-color: #FF001F; +} + +.shapeshift-form__address-input-error-message { + color: #FF001F; + font-family: Montserrat Light; + font-size: 12px; + height: 24px; + line-height: 18px; +} + .shapeshift-form__metadata { display: flex; flex-flow: row wrap; @@ -640,11 +651,11 @@ button.first-time-flow__button--tertiary:hover { } .shapeshift-form__metadata-wrapper:nth-child(odd) { - padding-right: 24px; + padding-right: 14px; } .shapeshift-form__metadata-label { - flex: 1 0 65%; + flex: 1 0 60%; } .shapeshift-form__metadata-value { @@ -654,3 +665,9 @@ button.first-time-flow__button--tertiary:hover { text-overflow: ellipsis; white-space: nowrap; } + +.shapeshift-form__qr-code { + display: flex; + flex-flow: row nowrap; + justify-content: center; +} diff --git a/mascara/src/app/shapeshift-form/index.js b/mascara/src/app/shapeshift-form/index.js new file mode 100644 index 000000000..15c7e95e1 --- /dev/null +++ b/mascara/src/app/shapeshift-form/index.js @@ -0,0 +1,217 @@ +import React, {Component, PropTypes} from 'react' +import classnames from 'classnames' +import {qrcode} from 'qrcode-npm' +import {connect} from 'react-redux' +import {shapeShiftSubview, pairUpdate, buyWithShapeShift} from '../../../../ui/app/actions' +import {isValidAddress} from '../../../../ui/app/util' + +export class ShapeShiftForm extends Component { + static propTypes = { + selectedAddress: PropTypes.string.isRequired, + btnClass: PropTypes.string.isRequired, + tokenExchangeRates: PropTypes.object.isRequired, + coinOptions: PropTypes.object.isRequired, + shapeShiftSubview: PropTypes.func.isRequired, + pairUpdate: PropTypes.func.isRequired, + buyWithShapeShift: PropTypes.func.isRequired, + }; + + state = { + depositCoin: 'btc', + refundAddress: '', + showQrCode: false, + depositAddress: '', + errorMessage: '', + isLoading: false, + }; + + componentWillMount () { + this.props.shapeShiftSubview() + } + + onCoinChange = e => { + const coin = e.target.value + this.setState({ + depositCoin: coin, + errorMessage: '', + }) + this.props.pairUpdate(coin) + } + + onBuyWithShapeShift = () => { + this.setState({ + isLoading: true, + showQrCode: true, + }) + + const { + buyWithShapeShift, + selectedAddress: withdrawal, + } = this.props + const { + refundAddress: returnAddress, + depositCoin, + } = this.state + const pair = `${depositCoin}_eth` + const data = { + withdrawal, + pair, + returnAddress, + // Public api key + 'apiKey': '803d1f5df2ed1b1476e4b9e6bcd089e34d8874595dda6a23b67d93c56ea9cc2445e98a6748b219b2b6ad654d9f075f1f1db139abfa93158c04e825db122c14b6', + } + + if (isValidAddress(withdrawal)) { + buyWithShapeShift(data) + .then(d => this.setState({ + showQrCode: true, + depositAddress: d.deposit, + isLoading: false, + })) + .catch(() => this.setState({ + showQrCode: false, + errorMessage: 'Invalid Request', + isLoading: false, + })) + } + } + + renderMetadata (label, value) { + return ( +
+
+ {label}: +
+
+ {value} +
+
+ ) + } + + renderMarketInfo () { + const { depositCoin } = this.state + const coinPair = `${depositCoin}_eth` + const { tokenExchangeRates } = this.props + const { + limit, + rate, + minimum, + } = tokenExchangeRates[coinPair] || {} + + return ( +
+ {this.renderMetadata('Status', limit ? 'Available' : 'Unavailable')} + {this.renderMetadata('Limit', limit)} + {this.renderMetadata('Exchange Rate', rate)} + {this.renderMetadata('Minimum', minimum)} +
+ ) + } + + renderQrCode () { + const { depositAddress, isLoading } = this.state + const qrImage = qrcode(4, 'M') + qrImage.addData(depositAddress) + qrImage.make() + + return ( +
+
+ Deposit your BTC to the address bellow: +
+
+ {isLoading + ? + :
+ } +
+ {this.renderMarketInfo()} +
+ ) + } + + render () { + const { coinOptions, btnClass } = this.props + const { depositCoin, errorMessage, showQrCode } = this.state + const coinPair = `${depositCoin}_eth` + const { tokenExchangeRates } = this.props + const token = tokenExchangeRates[coinPair] + + return showQrCode ? this.renderQrCode() : ( +
+
+
+
+
+ Deposit +
+ +
+
+
+
+ Receive +
+
+ ETH +
+
+
+
+
+ Your Refund Address +
+ this.setState({ + refundAddress: e.target.value, + errorMessage: '', + })} + /> +
+ {errorMessage} +
+
+ {this.renderMarketInfo()} +
+ +
+ ) + } +} + +export default connect( + ({ metamask: { coinOptions, tokenExchangeRates, selectedAddress } }) => ({ + coinOptions, tokenExchangeRates, selectedAddress, + }), + dispatch => ({ + shapeShiftSubview: () => dispatch(shapeShiftSubview()), + pairUpdate: coin => dispatch(pairUpdate(coin)), + buyWithShapeShift: data => dispatch(buyWithShapeShift(data)), + }) +)(ShapeShiftForm) diff --git a/ui/app/actions.js b/ui/app/actions.js index eb066a0e7..6a5a31bfb 100644 --- a/ui/app/actions.js +++ b/ui/app/actions.js @@ -138,6 +138,7 @@ var actions = { BUY_ETH: 'BUY_ETH', buyEth: buyEth, buyEthView: buyEthView, + buyWithShapeShift, BUY_ETH_VIEW: 'BUY_ETH_VIEW', COINBASE_SUBVIEW: 'COINBASE_SUBVIEW', coinBaseSubview: coinBaseSubview, @@ -977,6 +978,18 @@ function coinShiftRquest (data, marketData) { } } +function buyWithShapeShift (data) { + return dispatch => new Promise((resolve, reject) => { + shapeShiftRequest('shift', { method: 'POST', data}, (response) => { + if (response.error) { + return reject(response.error) + } + background.createShapeShiftTx(response.deposit, response.depositType) + return resolve(response) + }) + }) +} + function showQrView (data, message) { return { type: actions.SHOW_QR_VIEW, @@ -1010,9 +1023,14 @@ function shapeShiftRequest (query, options, cb) { options.method ? method = options.method : method = 'GET' var requestListner = function (request) { - queryResponse = JSON.parse(this.responseText) - cb ? cb(queryResponse) : null - return queryResponse + try { + queryResponse = JSON.parse(this.responseText) + cb ? cb(queryResponse) : null + return queryResponse + } catch (e) { + cb ? cb({error: e}) : null + return e + } } var shapShiftReq = new XMLHttpRequest() diff --git a/ui/app/reducers/app.js b/ui/app/reducers/app.js index 4b05b608d..6f08c6dc4 100644 --- a/ui/app/reducers/app.js +++ b/ui/app/reducers/app.js @@ -494,7 +494,6 @@ function reduceApp (state, action) { }, }) - case actions.ONBOARDING_BUY_ETH_VIEW: return extend(appState, { transForward: true, diff --git a/ui/app/reducers/metamask.js b/ui/app/reducers/metamask.js index 323539eef..85ac3e201 100644 --- a/ui/app/reducers/metamask.js +++ b/ui/app/reducers/metamask.js @@ -19,6 +19,8 @@ function reduceMetamask (state, action) { lastUnreadNotice: undefined, frequentRpcList: [], addressBook: [], + tokenExchangeRates: {}, + coinOptions: {}, }, state.metamask) switch (action.type) { @@ -132,6 +134,25 @@ function reduceMetamask (state, action) { conversionDate: action.value.conversionDate, }) + case actions.PAIR_UPDATE: + const { value: { marketinfo: pairMarketInfo } } = action + return extend(metamaskState, { + tokenExchangeRates: { + ...metamaskState.tokenExchangeRates, + [pairMarketInfo.pair]: pairMarketInfo, + }, + }) + + case actions.SHAPESHIFT_SUBVIEW: + const { value: { marketinfo, coinOptions } } = action + return extend(metamaskState, { + tokenExchangeRates: { + ...metamaskState.tokenExchangeRates, + [marketinfo.pair]: marketinfo, + }, + coinOptions, + }) + default: return metamaskState