diff options
Diffstat (limited to 'packages/website/ts/components/token_balances.tsx')
-rw-r--r-- | packages/website/ts/components/token_balances.tsx | 1066 |
1 files changed, 533 insertions, 533 deletions
diff --git a/packages/website/ts/components/token_balances.tsx b/packages/website/ts/components/token_balances.tsx index 2cef413c7..480652c34 100644 --- a/packages/website/ts/components/token_balances.tsx +++ b/packages/website/ts/components/token_balances.tsx @@ -23,16 +23,16 @@ import { TokenIcon } from 'ts/components/ui/token_icon'; import { trackedTokenStorage } from 'ts/local_storage/tracked_token_storage'; import { Dispatcher } from 'ts/redux/dispatcher'; import { - BalanceErrs, - BlockchainCallErrs, - BlockchainErrs, - EtherscanLinkSuffixes, - ScreenWidths, - Styles, - Token, - TokenByAddress, - TokenStateByAddress, - TokenVisibility, + BalanceErrs, + BlockchainCallErrs, + BlockchainErrs, + EtherscanLinkSuffixes, + ScreenWidths, + Styles, + Token, + TokenByAddress, + TokenStateByAddress, + TokenVisibility, } from 'ts/types'; import { colors } from 'ts/utils/colors'; import { configs } from 'ts/utils/configs'; @@ -53,554 +53,554 @@ const TOKEN_COL_SPAN_LG = 2; const TOKEN_COL_SPAN_SM = 1; const styles: Styles = { - bgColor: { - backgroundColor: colors.grey50, - }, + bgColor: { + backgroundColor: colors.grey50, + }, }; interface TokenBalancesProps { - blockchain: Blockchain; - blockchainErr: BlockchainErrs; - blockchainIsLoaded: boolean; - dispatcher: Dispatcher; - screenWidth: ScreenWidths; - tokenByAddress: TokenByAddress; - tokenStateByAddress: TokenStateByAddress; - userAddress: string; - userEtherBalance: BigNumber; - networkId: number; + blockchain: Blockchain; + blockchainErr: BlockchainErrs; + blockchainIsLoaded: boolean; + dispatcher: Dispatcher; + screenWidth: ScreenWidths; + tokenByAddress: TokenByAddress; + tokenStateByAddress: TokenStateByAddress; + userAddress: string; + userEtherBalance: BigNumber; + networkId: number; } interface TokenBalancesState { - errorType: BalanceErrs; - isBalanceSpinnerVisible: boolean; - isDharmaDialogVisible: boolean; - isZRXSpinnerVisible: boolean; - currentZrxBalance?: BigNumber; - isTokenPickerOpen: boolean; - isAddingToken: boolean; + errorType: BalanceErrs; + isBalanceSpinnerVisible: boolean; + isDharmaDialogVisible: boolean; + isZRXSpinnerVisible: boolean; + currentZrxBalance?: BigNumber; + isTokenPickerOpen: boolean; + isAddingToken: boolean; } export class TokenBalances extends React.Component<TokenBalancesProps, TokenBalancesState> { - public constructor(props: TokenBalancesProps) { - super(props); - this.state = { - errorType: undefined, - isBalanceSpinnerVisible: false, - isZRXSpinnerVisible: false, - isDharmaDialogVisible: DharmaLoanFrame.isAuthTokenPresent(), - isTokenPickerOpen: false, - isAddingToken: false, - }; - } - public componentWillReceiveProps(nextProps: TokenBalancesProps) { - if (nextProps.userEtherBalance !== this.props.userEtherBalance) { - if (this.state.isBalanceSpinnerVisible) { - const receivedAmount = nextProps.userEtherBalance.minus(this.props.userEtherBalance); - this.props.dispatcher.showFlashMessage(`Received ${receivedAmount.toString(10)} Kovan Ether`); - } - this.setState({ - isBalanceSpinnerVisible: false, - }); - } - const nextZrxToken = _.find(_.values(nextProps.tokenByAddress), t => t.symbol === ZRX_TOKEN_SYMBOL); - const nextZrxTokenBalance = nextProps.tokenStateByAddress[nextZrxToken.address].balance; - if (!_.isUndefined(this.state.currentZrxBalance) && !nextZrxTokenBalance.eq(this.state.currentZrxBalance)) { - if (this.state.isZRXSpinnerVisible) { - const receivedAmount = nextZrxTokenBalance.minus(this.state.currentZrxBalance); - const receiveAmountInUnits = ZeroEx.toUnitAmount(receivedAmount, constants.DECIMAL_PLACES_ZRX); - this.props.dispatcher.showFlashMessage(`Received ${receiveAmountInUnits.toString(10)} Kovan ZRX`); - } - this.setState({ - isZRXSpinnerVisible: false, - currentZrxBalance: undefined, - }); - } - } - public componentDidMount() { - window.scrollTo(0, 0); - } - public render() { - const errorDialogActions = [ - <FlatButton - key="errorOkBtn" - label="Ok" - primary={true} - onTouchTap={this._onErrorDialogToggle.bind(this, false)} - />, - ]; - const dharmaDialogActions = [ - <FlatButton - key="dharmaCloseBtn" - label="Close" - primary={true} - onTouchTap={this._onDharmaDialogToggle.bind(this, false)} - />, - ]; - const isTestNetwork = this.props.networkId === constants.NETWORK_ID_TESTNET; - const dharmaButtonColumnStyle = { - paddingLeft: 3, - display: isTestNetwork ? 'table-cell' : 'none', - }; - const stubColumnStyle = { - display: isTestNetwork ? 'none' : 'table-cell', - }; - const allTokenRowHeight = _.size(this.props.tokenByAddress) * TOKEN_TABLE_ROW_HEIGHT; - const tokenTableHeight = - allTokenRowHeight < MAX_TOKEN_TABLE_HEIGHT ? allTokenRowHeight : MAX_TOKEN_TABLE_HEIGHT; - const isSmallScreen = this.props.screenWidth === ScreenWidths.Sm; - const tokenColSpan = isSmallScreen ? TOKEN_COL_SPAN_SM : TOKEN_COL_SPAN_LG; - const dharmaLoanExplanation = - 'If you need access to larger amounts of ether,<br> \ + public constructor(props: TokenBalancesProps) { + super(props); + this.state = { + errorType: undefined, + isBalanceSpinnerVisible: false, + isZRXSpinnerVisible: false, + isDharmaDialogVisible: DharmaLoanFrame.isAuthTokenPresent(), + isTokenPickerOpen: false, + isAddingToken: false, + }; + } + public componentWillReceiveProps(nextProps: TokenBalancesProps) { + if (nextProps.userEtherBalance !== this.props.userEtherBalance) { + if (this.state.isBalanceSpinnerVisible) { + const receivedAmount = nextProps.userEtherBalance.minus(this.props.userEtherBalance); + this.props.dispatcher.showFlashMessage(`Received ${receivedAmount.toString(10)} Kovan Ether`); + } + this.setState({ + isBalanceSpinnerVisible: false, + }); + } + const nextZrxToken = _.find(_.values(nextProps.tokenByAddress), t => t.symbol === ZRX_TOKEN_SYMBOL); + const nextZrxTokenBalance = nextProps.tokenStateByAddress[nextZrxToken.address].balance; + if (!_.isUndefined(this.state.currentZrxBalance) && !nextZrxTokenBalance.eq(this.state.currentZrxBalance)) { + if (this.state.isZRXSpinnerVisible) { + const receivedAmount = nextZrxTokenBalance.minus(this.state.currentZrxBalance); + const receiveAmountInUnits = ZeroEx.toUnitAmount(receivedAmount, constants.DECIMAL_PLACES_ZRX); + this.props.dispatcher.showFlashMessage(`Received ${receiveAmountInUnits.toString(10)} Kovan ZRX`); + } + this.setState({ + isZRXSpinnerVisible: false, + currentZrxBalance: undefined, + }); + } + } + public componentDidMount() { + window.scrollTo(0, 0); + } + public render() { + const errorDialogActions = [ + <FlatButton + key="errorOkBtn" + label="Ok" + primary={true} + onTouchTap={this._onErrorDialogToggle.bind(this, false)} + />, + ]; + const dharmaDialogActions = [ + <FlatButton + key="dharmaCloseBtn" + label="Close" + primary={true} + onTouchTap={this._onDharmaDialogToggle.bind(this, false)} + />, + ]; + const isTestNetwork = this.props.networkId === constants.NETWORK_ID_TESTNET; + const dharmaButtonColumnStyle = { + paddingLeft: 3, + display: isTestNetwork ? 'table-cell' : 'none', + }; + const stubColumnStyle = { + display: isTestNetwork ? 'none' : 'table-cell', + }; + const allTokenRowHeight = _.size(this.props.tokenByAddress) * TOKEN_TABLE_ROW_HEIGHT; + const tokenTableHeight = + allTokenRowHeight < MAX_TOKEN_TABLE_HEIGHT ? allTokenRowHeight : MAX_TOKEN_TABLE_HEIGHT; + const isSmallScreen = this.props.screenWidth === ScreenWidths.Sm; + const tokenColSpan = isSmallScreen ? TOKEN_COL_SPAN_SM : TOKEN_COL_SPAN_LG; + const dharmaLoanExplanation = + 'If you need access to larger amounts of ether,<br> \ you can request a loan from the Dharma Loan<br> \ network. Your loan should be funded in 5<br> \ minutes or less.'; - const allowanceExplanation = - '0x smart contracts require access to your<br> \ + const allowanceExplanation = + '0x smart contracts require access to your<br> \ token balances in order to execute trades.<br> \ Toggling sets an allowance for the<br> \ smart contract so you can start trading that token.'; - return ( - <div className="lg-px4 md-px4 sm-px1 pb2"> - <h3>{isTestNetwork ? 'Test ether' : 'Ether'}</h3> - <Divider /> - <div className="pt2 pb2"> - {isTestNetwork - ? 'In order to try out the 0x Portal Dapp, request some test ether to pay for \ + return ( + <div className="lg-px4 md-px4 sm-px1 pb2"> + <h3>{isTestNetwork ? 'Test ether' : 'Ether'}</h3> + <Divider /> + <div className="pt2 pb2"> + {isTestNetwork + ? 'In order to try out the 0x Portal Dapp, request some test ether to pay for \ gas costs. It might take a bit of time for the test ether to show up.' - : 'Ether must be converted to Ether Tokens in order to be tradable via 0x. \ + : 'Ether must be converted to Ether Tokens in order to be tradable via 0x. \ You can convert between Ether and Ether Tokens from the "Wrap ETH" tab.'} - </div> - <Table selectable={false} style={styles.bgColor}> - <TableHeader displaySelectAll={false} adjustForCheckbox={false}> - <TableRow> - <TableHeaderColumn>Currency</TableHeaderColumn> - <TableHeaderColumn>Balance</TableHeaderColumn> - <TableRowColumn className="sm-hide xs-hide" style={stubColumnStyle} /> - {isTestNetwork && ( - <TableHeaderColumn style={{ paddingLeft: 3 }}> - {isSmallScreen ? 'Faucet' : 'Request from faucet'} - </TableHeaderColumn> - )} - {isTestNetwork && ( - <TableHeaderColumn style={dharmaButtonColumnStyle}> - {isSmallScreen ? 'Loan' : 'Request Dharma loan'} - <HelpTooltip style={{ paddingLeft: 4 }} explanation={dharmaLoanExplanation} /> - </TableHeaderColumn> - )} - </TableRow> - </TableHeader> - <TableBody displayRowCheckbox={false}> - <TableRow key="ETH"> - <TableRowColumn className="py1"> - <img style={{ width: ICON_DIMENSION, height: ICON_DIMENSION }} src={ETHER_ICON_PATH} /> - </TableRowColumn> - <TableRowColumn> - {this.props.userEtherBalance.toFixed(PRECISION)} ETH - {this.state.isBalanceSpinnerVisible && ( - <span className="pl1"> - <i className="zmdi zmdi-spinner zmdi-hc-spin" /> - </span> - )} - </TableRowColumn> - <TableRowColumn className="sm-hide xs-hide" style={stubColumnStyle} /> - {isTestNetwork && ( - <TableRowColumn style={{ paddingLeft: 3 }}> - <LifeCycleRaisedButton - labelReady="Request" - labelLoading="Sending..." - labelComplete="Sent!" - onClickAsyncFn={this._faucetRequestAsync.bind(this, true)} - /> - </TableRowColumn> - )} - {isTestNetwork && ( - <TableRowColumn style={dharmaButtonColumnStyle}> - <RaisedButton - label="Request" - style={{ width: '100%' }} - onTouchTap={this._onDharmaDialogToggle.bind(this)} - /> - </TableRowColumn> - )} - </TableRow> - </TableBody> - </Table> - <div className="clearfix" style={{ paddingBottom: 1 }}> - <div className="col col-10"> - <h3 className="pt2">{isTestNetwork ? 'Test tokens' : 'Tokens'}</h3> - </div> - <div className="col col-1 pt3 align-right"> - <FloatingActionButton mini={true} zDepth={0} onClick={this._onAddTokenClicked.bind(this)}> - <ContentAdd /> - </FloatingActionButton> - </div> - <div className="col col-1 pt3 align-right"> - <FloatingActionButton mini={true} zDepth={0} onClick={this._onRemoveTokenClicked.bind(this)}> - <ContentRemove /> - </FloatingActionButton> - </div> - </div> - <Divider /> - <div className="pt2 pb2"> - {isTestNetwork - ? "Mint some test tokens you'd like to use to generate or fill an order using 0x." - : "Set trading permissions for a token you'd like to start trading."} - </div> - <Table selectable={false} bodyStyle={{ height: tokenTableHeight }} style={styles.bgColor}> - <TableHeader displaySelectAll={false} adjustForCheckbox={false}> - <TableRow> - <TableHeaderColumn colSpan={tokenColSpan}>Token</TableHeaderColumn> - <TableHeaderColumn style={{ paddingLeft: 3 }}>Balance</TableHeaderColumn> - <TableHeaderColumn> - <div className="inline-block">Allowance</div> - <HelpTooltip style={{ paddingLeft: 4 }} explanation={allowanceExplanation} /> - </TableHeaderColumn> - <TableHeaderColumn>Action</TableHeaderColumn> - {this.props.screenWidth !== ScreenWidths.Sm && <TableHeaderColumn>Send</TableHeaderColumn>} - </TableRow> - </TableHeader> - <TableBody displayRowCheckbox={false}>{this._renderTokenTableRows()}</TableBody> - </Table> - <Dialog - title="Oh oh" - titleStyle={{ fontWeight: 100 }} - actions={errorDialogActions} - open={!_.isUndefined(this.state.errorType)} - onRequestClose={this._onErrorDialogToggle.bind(this, false)} - > - {this._renderErrorDialogBody()} - </Dialog> - <Dialog - title="Request Dharma Loan" - titleStyle={{ fontWeight: 100, backgroundColor: colors.white }} - bodyStyle={{ backgroundColor: colors.dharmaDarkGrey }} - actionsContainerStyle={{ backgroundColor: colors.white }} - autoScrollBodyContent={true} - actions={dharmaDialogActions} - open={this.state.isDharmaDialogVisible} - > - {this._renderDharmaLoanFrame()} - </Dialog> - <AssetPicker - userAddress={this.props.userAddress} - networkId={this.props.networkId} - blockchain={this.props.blockchain} - dispatcher={this.props.dispatcher} - isOpen={this.state.isTokenPickerOpen} - currentTokenAddress={''} - onTokenChosen={this._onAssetTokenPicked.bind(this)} - tokenByAddress={this.props.tokenByAddress} - tokenVisibility={this.state.isAddingToken ? TokenVisibility.UNTRACKED : TokenVisibility.TRACKED} - /> - </div> - ); - } - private _renderTokenTableRows() { - if (!this.props.blockchainIsLoaded || this.props.blockchainErr !== BlockchainErrs.NoError) { - return ''; - } - const isSmallScreen = this.props.screenWidth === ScreenWidths.Sm; - const tokenColSpan = isSmallScreen ? TOKEN_COL_SPAN_SM : TOKEN_COL_SPAN_LG; - const actionPaddingX = isSmallScreen ? 2 : 24; - const allTokens = _.values(this.props.tokenByAddress); - const trackedTokens = _.filter(allTokens, t => t.isTracked); - const trackedTokensStartingWithEtherToken = trackedTokens.sort( - firstBy((t: Token) => t.symbol !== ETHER_TOKEN_SYMBOL) - .thenBy((t: Token) => t.symbol !== ZRX_TOKEN_SYMBOL) - .thenBy('address'), - ); - const tableRows = _.map( - trackedTokensStartingWithEtherToken, - this._renderTokenRow.bind(this, tokenColSpan, actionPaddingX), - ); - return tableRows; - } - private _renderTokenRow(tokenColSpan: number, actionPaddingX: number, token: Token) { - const tokenState = this.props.tokenStateByAddress[token.address]; - const tokenLink = utils.getEtherScanLinkIfExists( - token.address, - this.props.networkId, - EtherscanLinkSuffixes.Address, - ); - const isMintable = - _.includes(configs.SYMBOLS_OF_MINTABLE_TOKENS, token.symbol) && - this.props.networkId !== constants.NETWORK_ID_MAINNET; - return ( - <TableRow key={token.address} style={{ height: TOKEN_TABLE_ROW_HEIGHT }}> - <TableRowColumn colSpan={tokenColSpan}> - {_.isUndefined(tokenLink) ? ( - this._renderTokenName(token) - ) : ( - <a href={tokenLink} target="_blank" style={{ textDecoration: 'none' }}> - {this._renderTokenName(token)} - </a> - )} - </TableRowColumn> - <TableRowColumn style={{ paddingRight: 3, paddingLeft: 3 }}> - {this._renderAmount(tokenState.balance, token.decimals)} {token.symbol} - {this.state.isZRXSpinnerVisible && - token.symbol === ZRX_TOKEN_SYMBOL && ( - <span className="pl1"> - <i className="zmdi zmdi-spinner zmdi-hc-spin" /> - </span> - )} - </TableRowColumn> - <TableRowColumn> - <AllowanceToggle - blockchain={this.props.blockchain} - dispatcher={this.props.dispatcher} - token={token} - tokenState={tokenState} - onErrorOccurred={this._onErrorOccurred.bind(this)} - userAddress={this.props.userAddress} - /> - </TableRowColumn> - <TableRowColumn style={{ paddingLeft: actionPaddingX, paddingRight: actionPaddingX }}> - {isMintable && ( - <LifeCycleRaisedButton - labelReady="Mint" - labelLoading={<span style={{ fontSize: 12 }}>Minting...</span>} - labelComplete="Minted!" - onClickAsyncFn={this._onMintTestTokensAsync.bind(this, token)} - /> - )} - {token.symbol === ZRX_TOKEN_SYMBOL && - this.props.networkId === constants.NETWORK_ID_TESTNET && ( - <LifeCycleRaisedButton - labelReady="Request" - labelLoading="Sending..." - labelComplete="Sent!" - onClickAsyncFn={this._faucetRequestAsync.bind(this, false)} - /> - )} - </TableRowColumn> - {this.props.screenWidth !== ScreenWidths.Sm && ( - <TableRowColumn - style={{ - paddingLeft: actionPaddingX, - paddingRight: actionPaddingX, - }} - > - <SendButton - blockchain={this.props.blockchain} - dispatcher={this.props.dispatcher} - token={token} - tokenState={tokenState} - onError={this._onSendFailed.bind(this)} - /> - </TableRowColumn> - )} - </TableRow> - ); - } - private _onAssetTokenPicked(tokenAddress: string) { - if (_.isEmpty(tokenAddress)) { - this.setState({ - isTokenPickerOpen: false, - }); - return; - } - const token = this.props.tokenByAddress[tokenAddress]; - const isDefaultTrackedToken = _.includes(configs.DEFAULT_TRACKED_TOKEN_SYMBOLS, token.symbol); - if (!this.state.isAddingToken && !isDefaultTrackedToken) { - if (token.isRegistered) { - // Remove the token from tracked tokens - const newToken = { - ...token, - isTracked: false, - }; - this.props.dispatcher.updateTokenByAddress([newToken]); - } else { - this.props.dispatcher.removeTokenToTokenByAddress(token); - } - this.props.dispatcher.removeFromTokenStateByAddress(tokenAddress); - trackedTokenStorage.removeTrackedToken(this.props.userAddress, this.props.networkId, tokenAddress); - } else if (isDefaultTrackedToken) { - this.props.dispatcher.showFlashMessage(`Cannot remove ${token.name} because it's a default token`); - } - this.setState({ - isTokenPickerOpen: false, - }); - } - private _onSendFailed() { - this.setState({ - errorType: BalanceErrs.sendFailed, - }); - } - private _renderAmount(amount: BigNumber, decimals: number) { - const unitAmount = ZeroEx.toUnitAmount(amount, decimals); - return unitAmount.toNumber().toFixed(PRECISION); - } - private _renderTokenName(token: Token) { - const tooltipId = `tooltip-${token.address}`; - return ( - <div className="flex"> - <TokenIcon token={token} diameter={ICON_DIMENSION} /> - <div data-tip={true} data-for={tooltipId} className="mt2 ml2 sm-hide xs-hide"> - {token.name} - </div> - <ReactTooltip id={tooltipId}>{token.address}</ReactTooltip> - </div> - ); - } - private _renderErrorDialogBody() { - switch (this.state.errorType) { - case BalanceErrs.incorrectNetworkForFaucet: - return ( - <div> - Our faucet can only send test Ether to addresses on the {constants.TESTNET_NAME} testnet - (networkId {constants.NETWORK_ID_TESTNET}). Please make sure you are connected to the{' '} - {constants.TESTNET_NAME} testnet and try requesting ether again. - </div> - ); + </div> + <Table selectable={false} style={styles.bgColor}> + <TableHeader displaySelectAll={false} adjustForCheckbox={false}> + <TableRow> + <TableHeaderColumn>Currency</TableHeaderColumn> + <TableHeaderColumn>Balance</TableHeaderColumn> + <TableRowColumn className="sm-hide xs-hide" style={stubColumnStyle} /> + {isTestNetwork && ( + <TableHeaderColumn style={{ paddingLeft: 3 }}> + {isSmallScreen ? 'Faucet' : 'Request from faucet'} + </TableHeaderColumn> + )} + {isTestNetwork && ( + <TableHeaderColumn style={dharmaButtonColumnStyle}> + {isSmallScreen ? 'Loan' : 'Request Dharma loan'} + <HelpTooltip style={{ paddingLeft: 4 }} explanation={dharmaLoanExplanation} /> + </TableHeaderColumn> + )} + </TableRow> + </TableHeader> + <TableBody displayRowCheckbox={false}> + <TableRow key="ETH"> + <TableRowColumn className="py1"> + <img style={{ width: ICON_DIMENSION, height: ICON_DIMENSION }} src={ETHER_ICON_PATH} /> + </TableRowColumn> + <TableRowColumn> + {this.props.userEtherBalance.toFixed(PRECISION)} ETH + {this.state.isBalanceSpinnerVisible && ( + <span className="pl1"> + <i className="zmdi zmdi-spinner zmdi-hc-spin" /> + </span> + )} + </TableRowColumn> + <TableRowColumn className="sm-hide xs-hide" style={stubColumnStyle} /> + {isTestNetwork && ( + <TableRowColumn style={{ paddingLeft: 3 }}> + <LifeCycleRaisedButton + labelReady="Request" + labelLoading="Sending..." + labelComplete="Sent!" + onClickAsyncFn={this._faucetRequestAsync.bind(this, true)} + /> + </TableRowColumn> + )} + {isTestNetwork && ( + <TableRowColumn style={dharmaButtonColumnStyle}> + <RaisedButton + label="Request" + style={{ width: '100%' }} + onTouchTap={this._onDharmaDialogToggle.bind(this)} + /> + </TableRowColumn> + )} + </TableRow> + </TableBody> + </Table> + <div className="clearfix" style={{ paddingBottom: 1 }}> + <div className="col col-10"> + <h3 className="pt2">{isTestNetwork ? 'Test tokens' : 'Tokens'}</h3> + </div> + <div className="col col-1 pt3 align-right"> + <FloatingActionButton mini={true} zDepth={0} onClick={this._onAddTokenClicked.bind(this)}> + <ContentAdd /> + </FloatingActionButton> + </div> + <div className="col col-1 pt3 align-right"> + <FloatingActionButton mini={true} zDepth={0} onClick={this._onRemoveTokenClicked.bind(this)}> + <ContentRemove /> + </FloatingActionButton> + </div> + </div> + <Divider /> + <div className="pt2 pb2"> + {isTestNetwork + ? "Mint some test tokens you'd like to use to generate or fill an order using 0x." + : "Set trading permissions for a token you'd like to start trading."} + </div> + <Table selectable={false} bodyStyle={{ height: tokenTableHeight }} style={styles.bgColor}> + <TableHeader displaySelectAll={false} adjustForCheckbox={false}> + <TableRow> + <TableHeaderColumn colSpan={tokenColSpan}>Token</TableHeaderColumn> + <TableHeaderColumn style={{ paddingLeft: 3 }}>Balance</TableHeaderColumn> + <TableHeaderColumn> + <div className="inline-block">Allowance</div> + <HelpTooltip style={{ paddingLeft: 4 }} explanation={allowanceExplanation} /> + </TableHeaderColumn> + <TableHeaderColumn>Action</TableHeaderColumn> + {this.props.screenWidth !== ScreenWidths.Sm && <TableHeaderColumn>Send</TableHeaderColumn>} + </TableRow> + </TableHeader> + <TableBody displayRowCheckbox={false}>{this._renderTokenTableRows()}</TableBody> + </Table> + <Dialog + title="Oh oh" + titleStyle={{ fontWeight: 100 }} + actions={errorDialogActions} + open={!_.isUndefined(this.state.errorType)} + onRequestClose={this._onErrorDialogToggle.bind(this, false)} + > + {this._renderErrorDialogBody()} + </Dialog> + <Dialog + title="Request Dharma Loan" + titleStyle={{ fontWeight: 100, backgroundColor: colors.white }} + bodyStyle={{ backgroundColor: colors.dharmaDarkGrey }} + actionsContainerStyle={{ backgroundColor: colors.white }} + autoScrollBodyContent={true} + actions={dharmaDialogActions} + open={this.state.isDharmaDialogVisible} + > + {this._renderDharmaLoanFrame()} + </Dialog> + <AssetPicker + userAddress={this.props.userAddress} + networkId={this.props.networkId} + blockchain={this.props.blockchain} + dispatcher={this.props.dispatcher} + isOpen={this.state.isTokenPickerOpen} + currentTokenAddress={''} + onTokenChosen={this._onAssetTokenPicked.bind(this)} + tokenByAddress={this.props.tokenByAddress} + tokenVisibility={this.state.isAddingToken ? TokenVisibility.UNTRACKED : TokenVisibility.TRACKED} + /> + </div> + ); + } + private _renderTokenTableRows() { + if (!this.props.blockchainIsLoaded || this.props.blockchainErr !== BlockchainErrs.NoError) { + return ''; + } + const isSmallScreen = this.props.screenWidth === ScreenWidths.Sm; + const tokenColSpan = isSmallScreen ? TOKEN_COL_SPAN_SM : TOKEN_COL_SPAN_LG; + const actionPaddingX = isSmallScreen ? 2 : 24; + const allTokens = _.values(this.props.tokenByAddress); + const trackedTokens = _.filter(allTokens, t => t.isTracked); + const trackedTokensStartingWithEtherToken = trackedTokens.sort( + firstBy((t: Token) => t.symbol !== ETHER_TOKEN_SYMBOL) + .thenBy((t: Token) => t.symbol !== ZRX_TOKEN_SYMBOL) + .thenBy('address'), + ); + const tableRows = _.map( + trackedTokensStartingWithEtherToken, + this._renderTokenRow.bind(this, tokenColSpan, actionPaddingX), + ); + return tableRows; + } + private _renderTokenRow(tokenColSpan: number, actionPaddingX: number, token: Token) { + const tokenState = this.props.tokenStateByAddress[token.address]; + const tokenLink = utils.getEtherScanLinkIfExists( + token.address, + this.props.networkId, + EtherscanLinkSuffixes.Address, + ); + const isMintable = + _.includes(configs.SYMBOLS_OF_MINTABLE_TOKENS, token.symbol) && + this.props.networkId !== constants.NETWORK_ID_MAINNET; + return ( + <TableRow key={token.address} style={{ height: TOKEN_TABLE_ROW_HEIGHT }}> + <TableRowColumn colSpan={tokenColSpan}> + {_.isUndefined(tokenLink) ? ( + this._renderTokenName(token) + ) : ( + <a href={tokenLink} target="_blank" style={{ textDecoration: 'none' }}> + {this._renderTokenName(token)} + </a> + )} + </TableRowColumn> + <TableRowColumn style={{ paddingRight: 3, paddingLeft: 3 }}> + {this._renderAmount(tokenState.balance, token.decimals)} {token.symbol} + {this.state.isZRXSpinnerVisible && + token.symbol === ZRX_TOKEN_SYMBOL && ( + <span className="pl1"> + <i className="zmdi zmdi-spinner zmdi-hc-spin" /> + </span> + )} + </TableRowColumn> + <TableRowColumn> + <AllowanceToggle + blockchain={this.props.blockchain} + dispatcher={this.props.dispatcher} + token={token} + tokenState={tokenState} + onErrorOccurred={this._onErrorOccurred.bind(this)} + userAddress={this.props.userAddress} + /> + </TableRowColumn> + <TableRowColumn style={{ paddingLeft: actionPaddingX, paddingRight: actionPaddingX }}> + {isMintable && ( + <LifeCycleRaisedButton + labelReady="Mint" + labelLoading={<span style={{ fontSize: 12 }}>Minting...</span>} + labelComplete="Minted!" + onClickAsyncFn={this._onMintTestTokensAsync.bind(this, token)} + /> + )} + {token.symbol === ZRX_TOKEN_SYMBOL && + this.props.networkId === constants.NETWORK_ID_TESTNET && ( + <LifeCycleRaisedButton + labelReady="Request" + labelLoading="Sending..." + labelComplete="Sent!" + onClickAsyncFn={this._faucetRequestAsync.bind(this, false)} + /> + )} + </TableRowColumn> + {this.props.screenWidth !== ScreenWidths.Sm && ( + <TableRowColumn + style={{ + paddingLeft: actionPaddingX, + paddingRight: actionPaddingX, + }} + > + <SendButton + blockchain={this.props.blockchain} + dispatcher={this.props.dispatcher} + token={token} + tokenState={tokenState} + onError={this._onSendFailed.bind(this)} + /> + </TableRowColumn> + )} + </TableRow> + ); + } + private _onAssetTokenPicked(tokenAddress: string) { + if (_.isEmpty(tokenAddress)) { + this.setState({ + isTokenPickerOpen: false, + }); + return; + } + const token = this.props.tokenByAddress[tokenAddress]; + const isDefaultTrackedToken = _.includes(configs.DEFAULT_TRACKED_TOKEN_SYMBOLS, token.symbol); + if (!this.state.isAddingToken && !isDefaultTrackedToken) { + if (token.isRegistered) { + // Remove the token from tracked tokens + const newToken = { + ...token, + isTracked: false, + }; + this.props.dispatcher.updateTokenByAddress([newToken]); + } else { + this.props.dispatcher.removeTokenToTokenByAddress(token); + } + this.props.dispatcher.removeFromTokenStateByAddress(tokenAddress); + trackedTokenStorage.removeTrackedToken(this.props.userAddress, this.props.networkId, tokenAddress); + } else if (isDefaultTrackedToken) { + this.props.dispatcher.showFlashMessage(`Cannot remove ${token.name} because it's a default token`); + } + this.setState({ + isTokenPickerOpen: false, + }); + } + private _onSendFailed() { + this.setState({ + errorType: BalanceErrs.sendFailed, + }); + } + private _renderAmount(amount: BigNumber, decimals: number) { + const unitAmount = ZeroEx.toUnitAmount(amount, decimals); + return unitAmount.toNumber().toFixed(PRECISION); + } + private _renderTokenName(token: Token) { + const tooltipId = `tooltip-${token.address}`; + return ( + <div className="flex"> + <TokenIcon token={token} diameter={ICON_DIMENSION} /> + <div data-tip={true} data-for={tooltipId} className="mt2 ml2 sm-hide xs-hide"> + {token.name} + </div> + <ReactTooltip id={tooltipId}>{token.address}</ReactTooltip> + </div> + ); + } + private _renderErrorDialogBody() { + switch (this.state.errorType) { + case BalanceErrs.incorrectNetworkForFaucet: + return ( + <div> + Our faucet can only send test Ether to addresses on the {constants.TESTNET_NAME} testnet + (networkId {constants.NETWORK_ID_TESTNET}). Please make sure you are connected to the{' '} + {constants.TESTNET_NAME} testnet and try requesting ether again. + </div> + ); - case BalanceErrs.faucetRequestFailed: - return ( - <div> - An unexpected error occurred while trying to request test Ether from our faucet. Please refresh - the page and try again. - </div> - ); + case BalanceErrs.faucetRequestFailed: + return ( + <div> + An unexpected error occurred while trying to request test Ether from our faucet. Please refresh + the page and try again. + </div> + ); - case BalanceErrs.faucetQueueIsFull: - return <div>Our test Ether faucet queue is full. Please try requesting test Ether again later.</div>; + case BalanceErrs.faucetQueueIsFull: + return <div>Our test Ether faucet queue is full. Please try requesting test Ether again later.</div>; - case BalanceErrs.mintingFailed: - return <div>Minting your test tokens failed unexpectedly. Please refresh the page and try again.</div>; + case BalanceErrs.mintingFailed: + return <div>Minting your test tokens failed unexpectedly. Please refresh the page and try again.</div>; - case BalanceErrs.allowanceSettingFailed: - return ( - <div> - An unexpected error occurred while trying to set your test token allowance. Please refresh the - page and try again. - </div> - ); + case BalanceErrs.allowanceSettingFailed: + return ( + <div> + An unexpected error occurred while trying to set your test token allowance. Please refresh the + page and try again. + </div> + ); - case undefined: - return null; // No error to show + case undefined: + return null; // No error to show - default: - throw utils.spawnSwitchErr('errorType', this.state.errorType); - } - } - private _renderDharmaLoanFrame() { - if (utils.isUserOnMobile()) { - return ( - <h4 style={{ textAlign: 'center' }}> - We apologize -- Dharma loan requests are not available on mobile yet. Please try again through your - desktop browser. - </h4> - ); - } else { - return ( - <DharmaLoanFrame - partner="0x" - env={utils.getCurrentEnvironment()} - screenWidth={this.props.screenWidth} - /> - ); - } - } - private _onErrorOccurred(errorType: BalanceErrs) { - this.setState({ - errorType, - }); - } - private async _onMintTestTokensAsync(token: Token): Promise<boolean> { - try { - await this.props.blockchain.mintTestTokensAsync(token); - const amount = ZeroEx.toUnitAmount(constants.MINT_AMOUNT, token.decimals); - this.props.dispatcher.showFlashMessage(`Successfully minted ${amount.toString(10)} ${token.symbol}`); - return true; - } catch (err) { - const errMsg = `${err}`; - if (_.includes(errMsg, BlockchainCallErrs.UserHasNoAssociatedAddresses)) { - this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true); - return false; - } - if (_.includes(errMsg, 'User denied transaction')) { - return false; - } - utils.consoleLog(`Unexpected error encountered: ${err}`); - utils.consoleLog(err.stack); - this.setState({ - errorType: BalanceErrs.mintingFailed, - }); - await errorReporter.reportAsync(err); - return false; - } - } - private async _faucetRequestAsync(isEtherRequest: boolean): Promise<boolean> { - if (this.props.userAddress === '') { - this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true); - return false; - } + default: + throw utils.spawnSwitchErr('errorType', this.state.errorType); + } + } + private _renderDharmaLoanFrame() { + if (utils.isUserOnMobile()) { + return ( + <h4 style={{ textAlign: 'center' }}> + We apologize -- Dharma loan requests are not available on mobile yet. Please try again through your + desktop browser. + </h4> + ); + } else { + return ( + <DharmaLoanFrame + partner="0x" + env={utils.getCurrentEnvironment()} + screenWidth={this.props.screenWidth} + /> + ); + } + } + private _onErrorOccurred(errorType: BalanceErrs) { + this.setState({ + errorType, + }); + } + private async _onMintTestTokensAsync(token: Token): Promise<boolean> { + try { + await this.props.blockchain.mintTestTokensAsync(token); + const amount = ZeroEx.toUnitAmount(constants.MINT_AMOUNT, token.decimals); + this.props.dispatcher.showFlashMessage(`Successfully minted ${amount.toString(10)} ${token.symbol}`); + return true; + } catch (err) { + const errMsg = `${err}`; + if (_.includes(errMsg, BlockchainCallErrs.UserHasNoAssociatedAddresses)) { + this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true); + return false; + } + if (_.includes(errMsg, 'User denied transaction')) { + return false; + } + utils.consoleLog(`Unexpected error encountered: ${err}`); + utils.consoleLog(err.stack); + this.setState({ + errorType: BalanceErrs.mintingFailed, + }); + await errorReporter.reportAsync(err); + return false; + } + } + private async _faucetRequestAsync(isEtherRequest: boolean): Promise<boolean> { + if (this.props.userAddress === '') { + this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true); + return false; + } - // If on another network other then the testnet our faucet serves test ether - // from, we must show user an error message - if (this.props.blockchain.networkId !== constants.NETWORK_ID_TESTNET) { - this.setState({ - errorType: BalanceErrs.incorrectNetworkForFaucet, - }); - return false; - } + // If on another network other then the testnet our faucet serves test ether + // from, we must show user an error message + if (this.props.blockchain.networkId !== constants.NETWORK_ID_TESTNET) { + this.setState({ + errorType: BalanceErrs.incorrectNetworkForFaucet, + }); + return false; + } - await utils.sleepAsync(ARTIFICIAL_FAUCET_REQUEST_DELAY); + await utils.sleepAsync(ARTIFICIAL_FAUCET_REQUEST_DELAY); - const segment = isEtherRequest ? 'ether' : 'zrx'; - const response = await fetch(`${constants.URL_ETHER_FAUCET}/${segment}/${this.props.userAddress}`); - const responseBody = await response.text(); - if (response.status !== constants.SUCCESS_STATUS) { - utils.consoleLog(`Unexpected status code: ${response.status} -> ${responseBody}`); - const errorType = - response.status === constants.UNAVAILABLE_STATUS - ? BalanceErrs.faucetQueueIsFull - : BalanceErrs.faucetRequestFailed; - this.setState({ - errorType, - }); - await errorReporter.reportAsync(new Error(`Faucet returned non-200: ${JSON.stringify(response)}`)); - return false; - } + const segment = isEtherRequest ? 'ether' : 'zrx'; + const response = await fetch(`${constants.URL_ETHER_FAUCET}/${segment}/${this.props.userAddress}`); + const responseBody = await response.text(); + if (response.status !== constants.SUCCESS_STATUS) { + utils.consoleLog(`Unexpected status code: ${response.status} -> ${responseBody}`); + const errorType = + response.status === constants.UNAVAILABLE_STATUS + ? BalanceErrs.faucetQueueIsFull + : BalanceErrs.faucetRequestFailed; + this.setState({ + errorType, + }); + await errorReporter.reportAsync(new Error(`Faucet returned non-200: ${JSON.stringify(response)}`)); + return false; + } - if (isEtherRequest) { - this.setState({ - isBalanceSpinnerVisible: true, - }); - } else { - const tokens = _.values(this.props.tokenByAddress); - const zrxToken = _.find(tokens, t => t.symbol === ZRX_TOKEN_SYMBOL); - const zrxTokenState = this.props.tokenStateByAddress[zrxToken.address]; - this.setState({ - isZRXSpinnerVisible: true, - currentZrxBalance: zrxTokenState.balance, - }); - // tslint:disable-next-line:no-floating-promises - this.props.blockchain.pollTokenBalanceAsync(zrxToken); - } - return true; - } - private _onErrorDialogToggle(isOpen: boolean) { - this.setState({ - errorType: undefined, - }); - } - private _onDharmaDialogToggle() { - this.setState({ - isDharmaDialogVisible: !this.state.isDharmaDialogVisible, - }); - } - private _onAddTokenClicked() { - this.setState({ - isTokenPickerOpen: true, - isAddingToken: true, - }); - } - private _onRemoveTokenClicked() { - this.setState({ - isTokenPickerOpen: true, - isAddingToken: false, - }); - } + if (isEtherRequest) { + this.setState({ + isBalanceSpinnerVisible: true, + }); + } else { + const tokens = _.values(this.props.tokenByAddress); + const zrxToken = _.find(tokens, t => t.symbol === ZRX_TOKEN_SYMBOL); + const zrxTokenState = this.props.tokenStateByAddress[zrxToken.address]; + this.setState({ + isZRXSpinnerVisible: true, + currentZrxBalance: zrxTokenState.balance, + }); + // tslint:disable-next-line:no-floating-promises + this.props.blockchain.pollTokenBalanceAsync(zrxToken); + } + return true; + } + private _onErrorDialogToggle(isOpen: boolean) { + this.setState({ + errorType: undefined, + }); + } + private _onDharmaDialogToggle() { + this.setState({ + isDharmaDialogVisible: !this.state.isDharmaDialogVisible, + }); + } + private _onAddTokenClicked() { + this.setState({ + isTokenPickerOpen: true, + isAddingToken: true, + }); + } + private _onRemoveTokenClicked() { + this.setState({ + isTokenPickerOpen: true, + isAddingToken: false, + }); + } } // tslint:disable:max-file-line-count |