diff options
author | Brandon Millman <brandon.millman@gmail.com> | 2018-02-01 07:30:09 +0800 |
---|---|---|
committer | Brandon Millman <brandon.millman@gmail.com> | 2018-02-01 07:30:09 +0800 |
commit | 03cb7233dc5b8556952b4481f87a292e0fca1acf (patch) | |
tree | 4c203211a7ce7b0f44ebc45bb6c40621d4ee5b7e /packages/website/ts/components | |
parent | 3a1ca32ff172f735e4b69f125fea4237c83643f0 (diff) | |
parent | 6682abf89dcdf566f05f8d88cb6af06c4bb1f6a2 (diff) | |
download | dexon-sol-tools-03cb7233dc5b8556952b4481f87a292e0fca1acf.tar dexon-sol-tools-03cb7233dc5b8556952b4481f87a292e0fca1acf.tar.gz dexon-sol-tools-03cb7233dc5b8556952b4481f87a292e0fca1acf.tar.bz2 dexon-sol-tools-03cb7233dc5b8556952b4481f87a292e0fca1acf.tar.lz dexon-sol-tools-03cb7233dc5b8556952b4481f87a292e0fca1acf.tar.xz dexon-sol-tools-03cb7233dc5b8556952b4481f87a292e0fca1acf.tar.zst dexon-sol-tools-03cb7233dc5b8556952b4481f87a292e0fca1acf.zip |
Merge branch 'development' into feature/testnet-faucets/order-dispenser
* development: (49 commits)
Prettier
Updated contract generation in 0x to new abi-gen CLI
Add PR number to changelog
Fix lint errors
Removed deprecated CLI options
Add protected keyword to underscore lint rule
Remove unused prop
Fix prettier
Uppercase Networks enum values
Make default gasPrice more readable
Fix prettier mess
Fix linter errors
Shrink img
Fix all setState calls after unmounted errors. Decided to create our own flag rather then using a cancellablePromise since there was little to be gained from this alternative
Fix bug where we were return undefined instead of the empty object
Default the derivation path to that found in the Ledger subprovider
Add browser data to dialog info
Add Rinkeby support
Pass in whether we want the personal message prefix appended and never append it for Ledger. This fixes signing when Ledger is used and the backing node is not Parity
Wholesale replace the tokenByAddress and de-dup properly
...
Diffstat (limited to 'packages/website/ts/components')
24 files changed, 869 insertions, 285 deletions
diff --git a/packages/website/ts/components/dialogs/blockchain_err_dialog.tsx b/packages/website/ts/components/dialogs/blockchain_err_dialog.tsx index f555ca6b1..278e2bbf5 100644 --- a/packages/website/ts/components/dialogs/blockchain_err_dialog.tsx +++ b/packages/website/ts/components/dialogs/blockchain_err_dialog.tsx @@ -3,7 +3,7 @@ import Dialog from 'material-ui/Dialog'; import FlatButton from 'material-ui/FlatButton'; import * as React from 'react'; import { Blockchain } from 'ts/blockchain'; -import { BlockchainErrs } from 'ts/types'; +import { BlockchainErrs, Networks } from 'ts/types'; import { colors } from 'ts/utils/colors'; import { configs } from 'ts/utils/configs'; import { constants } from 'ts/utils/constants'; @@ -129,7 +129,7 @@ export class BlockchainErrDialog extends React.Component<BlockchainErrDialogProp <div> The 0x smart contracts are not deployed on the Ethereum network you are currently connected to (network Id: {this.props.networkId}). In order to use the 0x portal dApp, please connect to the{' '} - {constants.TESTNET_NAME} testnet (network Id: {constants.NETWORK_ID_TESTNET}) + {Networks.Kovan} testnet (network Id: {constants.NETWORK_ID_KOVAN}) {configs.IS_MAINNET_ENABLED ? ` or ${constants.MAINNET_NAME} (network Id: ${constants.NETWORK_ID_MAINNET}).` : `.`} diff --git a/packages/website/ts/components/dialogs/eth_weth_conversion_dialog.tsx b/packages/website/ts/components/dialogs/eth_weth_conversion_dialog.tsx index 661cc1d8c..acd4a7110 100644 --- a/packages/website/ts/components/dialogs/eth_weth_conversion_dialog.tsx +++ b/packages/website/ts/components/dialogs/eth_weth_conversion_dialog.tsx @@ -2,38 +2,55 @@ import { BigNumber } from '@0xproject/utils'; import Dialog from 'material-ui/Dialog'; import FlatButton from 'material-ui/FlatButton'; import * as React from 'react'; +import { Blockchain } from 'ts/blockchain'; import { EthAmountInput } from 'ts/components/inputs/eth_amount_input'; import { TokenAmountInput } from 'ts/components/inputs/token_amount_input'; -import { Side, Token, TokenState } from 'ts/types'; +import { Side, Token } from 'ts/types'; import { colors } from 'ts/utils/colors'; interface EthWethConversionDialogProps { + blockchain: Blockchain; + userAddress: string; + networkId: number; direction: Side; onComplete: (direction: Side, value: BigNumber) => void; onCancelled: () => void; isOpen: boolean; token: Token; - tokenState: TokenState; etherBalance: BigNumber; + lastForceTokenStateRefetch: number; } interface EthWethConversionDialogState { value?: BigNumber; shouldShowIncompleteErrs: boolean; hasErrors: boolean; + isEthTokenBalanceLoaded: boolean; + ethTokenBalance: BigNumber; } export class EthWethConversionDialog extends React.Component< EthWethConversionDialogProps, EthWethConversionDialogState > { + private _isUnmounted: boolean; constructor() { super(); + this._isUnmounted = false; this.state = { shouldShowIncompleteErrs: false, hasErrors: false, + isEthTokenBalanceLoaded: false, + ethTokenBalance: new BigNumber(0), }; } + public componentWillMount() { + // tslint:disable-next-line:no-floating-promises + this._fetchEthTokenBalanceAsync(); + } + public componentWillUnmount() { + this._isUnmounted = true; + } public render() { const convertDialogActions = [ <FlatButton key="cancel" label="Cancel" onTouchTap={this._onCancel.bind(this)} />, @@ -72,8 +89,11 @@ export class EthWethConversionDialog extends React.Component< <div className="pt2 mx-auto" style={{ width: 245 }}> {this.props.direction === Side.Receive ? ( <TokenAmountInput + lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} + blockchain={this.props.blockchain} + userAddress={this.props.userAddress} + networkId={this.props.networkId} token={this.props.token} - tokenState={this.props.tokenState} shouldShowIncompleteErrs={this.state.shouldShowIncompleteErrs} shouldCheckBalance={true} shouldCheckAllowance={false} @@ -93,19 +113,20 @@ export class EthWethConversionDialog extends React.Component< )} <div className="pt1" style={{ fontSize: 12 }}> <div className="left">1 ETH = 1 WETH</div> - {this.props.direction === Side.Receive && ( - <div - className="right" - onClick={this._onMaxClick.bind(this)} - style={{ - color: colors.darkBlue, - textDecoration: 'underline', - cursor: 'pointer', - }} - > - Max - </div> - )} + {this.props.direction === Side.Receive && + this.state.isEthTokenBalanceLoaded && ( + <div + className="right" + onClick={this._onMaxClick.bind(this)} + style={{ + color: colors.darkBlue, + textDecoration: 'underline', + cursor: 'pointer', + }} + > + Max + </div> + )} </div> </div> </div> @@ -132,7 +153,7 @@ export class EthWethConversionDialog extends React.Component< } private _onMaxClick() { this.setState({ - value: this.props.tokenState.balance, + value: this.state.ethTokenBalance, }); } private _onValueChange(isValid: boolean, amount?: BigNumber) { @@ -160,4 +181,16 @@ export class EthWethConversionDialog extends React.Component< }); this.props.onCancelled(); } + private async _fetchEthTokenBalanceAsync() { + const [balance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync( + this.props.userAddress, + this.props.token.address, + ); + if (!this._isUnmounted) { + this.setState({ + isEthTokenBalanceLoaded: true, + ethTokenBalance: balance, + }); + } + } } diff --git a/packages/website/ts/components/dialogs/ledger_config_dialog.tsx b/packages/website/ts/components/dialogs/ledger_config_dialog.tsx index 60db93c52..bc5f05241 100644 --- a/packages/website/ts/components/dialogs/ledger_config_dialog.tsx +++ b/packages/website/ts/components/dialogs/ledger_config_dialog.tsx @@ -7,8 +7,10 @@ import TextField from 'material-ui/TextField'; import * as React from 'react'; import ReactTooltip = require('react-tooltip'); import { Blockchain } from 'ts/blockchain'; +import { NetworkDropDown } from 'ts/components/dropdowns/network_drop_down'; import { LifeCycleRaisedButton } from 'ts/components/ui/lifecycle_raised_button'; import { Dispatcher } from 'ts/redux/dispatcher'; +import { ProviderType } from 'ts/types'; import { colors } from 'ts/utils/colors'; import { configs } from 'ts/utils/configs'; import { constants } from 'ts/utils/constants'; @@ -27,27 +29,33 @@ interface LedgerConfigDialogProps { dispatcher: Dispatcher; blockchain: Blockchain; networkId: number; + providerType: ProviderType; } interface LedgerConfigDialogState { - didConnectFail: boolean; + connectionErrMsg: string; stepIndex: LedgerSteps; userAddresses: string[]; addressBalances: BigNumber[]; derivationPath: string; derivationErrMsg: string; + preferredNetworkId: number; } export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps, LedgerConfigDialogState> { constructor(props: LedgerConfigDialogProps) { super(props); + const derivationPathIfExists = props.blockchain.getLedgerDerivationPathIfExists(); this.state = { - didConnectFail: false, + connectionErrMsg: '', stepIndex: LedgerSteps.CONNECT, userAddresses: [], addressBalances: [], - derivationPath: configs.DEFAULT_DERIVATION_PATH, + derivationPath: _.isUndefined(derivationPathIfExists) + ? configs.DEFAULT_DERIVATION_PATH + : derivationPathIfExists, derivationErrMsg: '', + preferredNetworkId: props.networkId, }; } public render() { @@ -74,19 +82,28 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps, ); } private _renderConnectStep() { + const networkIds = _.values(constants.NETWORK_ID_BY_NAME); return ( <div> <div className="h4 pt3">Follow these instructions before proceeding:</div> - <ol> + <ol className="mb0"> <li className="pb1">Connect your Ledger Nano S & Open the Ethereum application</li> - <li className="pb1">Verify that Browser Support is enabled in Settings</li> + <li className="pb1">Verify that "Browser Support" AND "Contract Data" are enabled in Settings</li> <li className="pb1"> If no Browser Support is found in settings, verify that you have{' '} <a href="https://www.ledgerwallet.com/apps/manager" target="_blank"> Firmware >1.2 </a> </li> + <li>Choose your desired network:</li> </ol> + <div className="pb2"> + <NetworkDropDown + updateSelectedNetwork={this._onSelectedNetworkUpdated.bind(this)} + selectedNetworkId={this.state.preferredNetworkId} + avialableNetworkIds={networkIds} + /> + </div> <div className="center pb3"> <LifeCycleRaisedButton isPrimary={true} @@ -95,9 +112,9 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps, labelComplete="Connected!" onClickAsyncFn={this._onConnectLedgerClickAsync.bind(this, true)} /> - {this.state.didConnectFail && ( + {!_.isEmpty(this.state.connectionErrMsg) && ( <div className="pt2 left-align" style={{ color: colors.red200 }}> - Failed to connect. Follow the instructions and try again. + {this.state.connectionErrMsg} </div> )} </div> @@ -172,7 +189,8 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps, } private _onClose() { this.setState({ - didConnectFail: false, + connectionErrMsg: '', + stepIndex: LedgerSteps.CONNECT, }); const isOpen = false; this.props.toggleDialogFn(isOpen); @@ -184,6 +202,8 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps, const selectAddressBalance = this.state.addressBalances[selectedRowIndex]; this.props.dispatcher.updateUserAddress(selectedAddress); this.props.blockchain.updateWeb3WrapperPrevUserAddress(selectedAddress); + // tslint:disable-next-line:no-floating-promises + this.props.blockchain.fetchTokenInformationAsync(); this.props.dispatcher.updateUserEtherBalance(selectAddressBalance); this.setState({ stepIndex: LedgerSteps.CONNECT, @@ -219,7 +239,7 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps, } catch (err) { utils.consoleLog(`Ledger error: ${JSON.stringify(err)}`); this.setState({ - didConnectFail: true, + connectionErrMsg: 'Failed to connect. Follow the instructions and try again.', }); return false; } @@ -241,6 +261,22 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps, }); } private async _onConnectLedgerClickAsync() { + const isU2FSupported = await utils.isU2FSupportedAsync(); + if (!isU2FSupported) { + utils.consoleLog(`U2F not supported in this browser`); + this.setState({ + connectionErrMsg: 'U2F not supported by this browser. Try using Chrome.', + }); + return false; + } + + if ( + this.props.providerType !== ProviderType.Ledger || + (this.props.providerType === ProviderType.Ledger && this.props.networkId !== this.state.preferredNetworkId) + ) { + await this.props.blockchain.updateProviderToLedgerAsync(this.state.preferredNetworkId); + } + const didSucceed = await this._fetchAddressesAndBalancesAsync(); if (didSucceed) { this.setState({ @@ -258,4 +294,9 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps, } return userAddresses; } + private _onSelectedNetworkUpdated(e: any, index: number, networkId: number) { + this.setState({ + preferredNetworkId: networkId, + }); + } } diff --git a/packages/website/ts/components/dialogs/send_dialog.tsx b/packages/website/ts/components/dialogs/send_dialog.tsx index b3dbce598..d44dd9aab 100644 --- a/packages/website/ts/components/dialogs/send_dialog.tsx +++ b/packages/website/ts/components/dialogs/send_dialog.tsx @@ -3,16 +3,20 @@ import * as _ from 'lodash'; import Dialog from 'material-ui/Dialog'; import FlatButton from 'material-ui/FlatButton'; import * as React from 'react'; +import { Blockchain } from 'ts/blockchain'; import { AddressInput } from 'ts/components/inputs/address_input'; import { TokenAmountInput } from 'ts/components/inputs/token_amount_input'; -import { Token, TokenState } from 'ts/types'; +import { Token } from 'ts/types'; interface SendDialogProps { + blockchain: Blockchain; + userAddress: string; + networkId: number; onComplete: (recipient: string, value: BigNumber) => void; onCancelled: () => void; isOpen: boolean; token: Token; - tokenState: TokenState; + lastForceTokenStateRefetch: number; } interface SendDialogState { @@ -66,15 +70,18 @@ export class SendDialog extends React.Component<SendDialogProps, SendDialogState /> </div> <TokenAmountInput + blockchain={this.props.blockchain} + userAddress={this.props.userAddress} + networkId={this.props.networkId} label="Amount to send" token={this.props.token} - tokenState={this.props.tokenState} shouldShowIncompleteErrs={this.state.shouldShowIncompleteErrs} shouldCheckBalance={true} shouldCheckAllowance={false} onChange={this._onValueChange.bind(this)} amount={this.state.value} onVisitBalancesPageClick={this.props.onCancelled} + lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} /> </div> ); diff --git a/packages/website/ts/components/dialogs/track_token_confirmation_dialog.tsx b/packages/website/ts/components/dialogs/track_token_confirmation_dialog.tsx index 3f29d46f8..bb7e3ed1a 100644 --- a/packages/website/ts/components/dialogs/track_token_confirmation_dialog.tsx +++ b/packages/website/ts/components/dialogs/track_token_confirmation_dialog.tsx @@ -82,16 +82,6 @@ export class TrackTokenConfirmationDialog extends React.Component< newTokenEntry.isTracked = true; trackedTokenStorage.addTrackedTokenToUser(this.props.userAddress, this.props.networkId, newTokenEntry); this.props.dispatcher.updateTokenByAddress([newTokenEntry]); - - const [balance, allowance] = await this.props.blockchain.getCurrentUserTokenBalanceAndAllowanceAsync( - token.address, - ); - this.props.dispatcher.updateTokenStateByAddress({ - [token.address]: { - balance, - allowance, - }, - }); } this.setState({ diff --git a/packages/website/ts/components/dropdowns/network_drop_down.tsx b/packages/website/ts/components/dropdowns/network_drop_down.tsx new file mode 100644 index 000000000..28ec28ed5 --- /dev/null +++ b/packages/website/ts/components/dropdowns/network_drop_down.tsx @@ -0,0 +1,40 @@ +import * as _ from 'lodash'; +import DropDownMenu from 'material-ui/DropDownMenu'; +import MenuItem from 'material-ui/MenuItem'; +import * as React from 'react'; +import { constants } from 'ts/utils/constants'; + +interface NetworkDropDownProps { + updateSelectedNetwork: (e: any, index: number, value: number) => void; + selectedNetworkId: number; + avialableNetworkIds: number[]; +} + +interface NetworkDropDownState {} + +export class NetworkDropDown extends React.Component<NetworkDropDownProps, NetworkDropDownState> { + public render() { + return ( + <div className="mx-auto" style={{ width: 120 }}> + <DropDownMenu value={this.props.selectedNetworkId} onChange={this.props.updateSelectedNetwork}> + {this._renderDropDownItems()} + </DropDownMenu> + </div> + ); + } + private _renderDropDownItems() { + const items = _.map(this.props.avialableNetworkIds, networkId => { + const networkName = constants.NETWORK_NAME_BY_ID[networkId]; + const primaryText = ( + <div className="flex"> + <div className="pr1" style={{ width: 14, paddingTop: 2 }}> + <img src={`/images/network_icons/${networkName.toLowerCase()}.png`} style={{ width: 14 }} /> + </div> + <div>{networkName}</div> + </div> + ); + return <MenuItem key={networkId} value={networkId} primaryText={primaryText} />; + }); + return items; + } +} diff --git a/packages/website/ts/components/eth_weth_conversion_button.tsx b/packages/website/ts/components/eth_weth_conversion_button.tsx index 300e71f1f..62bafdba7 100644 --- a/packages/website/ts/components/eth_weth_conversion_button.tsx +++ b/packages/website/ts/components/eth_weth_conversion_button.tsx @@ -6,21 +6,24 @@ import * as React from 'react'; import { Blockchain } from 'ts/blockchain'; import { EthWethConversionDialog } from 'ts/components/dialogs/eth_weth_conversion_dialog'; import { Dispatcher } from 'ts/redux/dispatcher'; -import { BlockchainCallErrs, Side, Token, TokenState } from 'ts/types'; +import { BlockchainCallErrs, Side, Token } from 'ts/types'; import { constants } from 'ts/utils/constants'; import { errorReporter } from 'ts/utils/error_reporter'; import { utils } from 'ts/utils/utils'; interface EthWethConversionButtonProps { + userAddress: string; + networkId: number; direction: Side; ethToken: Token; - ethTokenState: TokenState; dispatcher: Dispatcher; blockchain: Blockchain; userEtherBalance: BigNumber; isOutdatedWrappedEther: boolean; onConversionSuccessful?: () => void; isDisabled?: boolean; + lastForceTokenStateRefetch: number; + refetchEthTokenStateAsync: () => Promise<void>; } interface EthWethConversionButtonState { @@ -64,13 +67,16 @@ export class EthWethConversionButton extends React.Component< onClick={this._toggleConversionDialog.bind(this)} /> <EthWethConversionDialog + blockchain={this.props.blockchain} + userAddress={this.props.userAddress} + networkId={this.props.networkId} direction={this.props.direction} isOpen={this.state.isEthConversionDialogVisible} onComplete={this._onConversionAmountSelectedAsync.bind(this)} onCancelled={this._toggleConversionDialog.bind(this)} etherBalance={this.props.userEtherBalance} token={this.props.ethToken} - tokenState={this.props.ethTokenState} + lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} /> </div> ); @@ -86,29 +92,25 @@ export class EthWethConversionButton extends React.Component< }); this._toggleConversionDialog(); const token = this.props.ethToken; - const tokenState = this.props.ethTokenState; - let balance = tokenState.balance; try { if (direction === Side.Deposit) { await this.props.blockchain.convertEthToWrappedEthTokensAsync(token.address, value); const ethAmount = ZeroEx.toUnitAmount(value, constants.DECIMAL_PLACES_ETH); this.props.dispatcher.showFlashMessage(`Successfully wrapped ${ethAmount.toString()} ETH to WETH`); - balance = balance.plus(value); } else { await this.props.blockchain.convertWrappedEthTokensToEthAsync(token.address, value); const tokenAmount = ZeroEx.toUnitAmount(value, token.decimals); this.props.dispatcher.showFlashMessage(`Successfully unwrapped ${tokenAmount.toString()} WETH to ETH`); - balance = balance.minus(value); } if (!this.props.isOutdatedWrappedEther) { - this.props.dispatcher.replaceTokenBalanceByAddress(token.address, balance); + await this.props.refetchEthTokenStateAsync(); } this.props.onConversionSuccessful(); } catch (err) { const errMsg = `${err}`; if (_.includes(errMsg, BlockchainCallErrs.UserHasNoAssociatedAddresses)) { this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true); - } else if (!_.includes(errMsg, 'User denied transaction')) { + } else if (!utils.didUserDenyWeb3Request(errMsg)) { utils.consoleLog(`Unexpected error encountered: ${err}`); utils.consoleLog(err.stack); const errorMsg = diff --git a/packages/website/ts/components/eth_wrappers.tsx b/packages/website/ts/components/eth_wrappers.tsx index d074ec787..c2cdf6751 100644 --- a/packages/website/ts/components/eth_wrappers.tsx +++ b/packages/website/ts/components/eth_wrappers.tsx @@ -16,7 +16,6 @@ import { Token, TokenByAddress, TokenState, - TokenStateByAddress, } from 'ts/types'; import { colors } from 'ts/utils/colors'; import { configs } from 'ts/utils/configs'; @@ -41,19 +40,23 @@ interface EthWrappersProps { blockchain: Blockchain; dispatcher: Dispatcher; tokenByAddress: TokenByAddress; - tokenStateByAddress: TokenStateByAddress; userAddress: string; userEtherBalance: BigNumber; + lastForceTokenStateRefetch: number; } interface EthWrappersState { + ethTokenState: TokenState; + isWethStateLoaded: boolean; outdatedWETHAddressToIsStateLoaded: OutdatedWETHAddressToIsStateLoaded; outdatedWETHStateByAddress: OutdatedWETHStateByAddress; } export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersState> { + private _isUnmounted: boolean; constructor(props: EthWrappersProps) { super(props); + this._isUnmounted = false; const outdatedWETHAddresses = this._getOutdatedWETHAddresses(); const outdatedWETHAddressToIsStateLoaded: OutdatedWETHAddressToIsStateLoaded = {}; const outdatedWETHStateByAddress: OutdatedWETHStateByAddress = {}; @@ -67,18 +70,34 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt this.state = { outdatedWETHAddressToIsStateLoaded, outdatedWETHStateByAddress, + isWethStateLoaded: false, + ethTokenState: { + balance: new BigNumber(0), + allowance: new BigNumber(0), + }, }; } + public componentWillReceiveProps(nextProps: EthWrappersProps) { + if ( + nextProps.userAddress !== this.props.userAddress || + nextProps.networkId !== this.props.networkId || + nextProps.lastForceTokenStateRefetch !== this.props.lastForceTokenStateRefetch + ) { + // tslint:disable-next-line:no-floating-promises + this._fetchWETHStateAsync(); + } + } public componentDidMount() { window.scrollTo(0, 0); // tslint:disable-next-line:no-floating-promises - this._fetchOutdatedWETHStateAsync(); + this._fetchWETHStateAsync(); + } + public componentWillUnmount() { + this._isUnmounted = true; } public render() { - const tokens = _.values(this.props.tokenByAddress); - const etherToken = _.find(tokens, { symbol: 'WETH' }); - const etherTokenState = this.props.tokenStateByAddress[etherToken.address]; - const wethBalance = ZeroEx.toUnitAmount(etherTokenState.balance, constants.DECIMAL_PLACES_ETH); + const etherToken = this._getEthToken(); + const wethBalance = ZeroEx.toUnitAmount(this.state.ethTokenState.balance, constants.DECIMAL_PLACES_ETH); const isBidirectional = true; const etherscanUrl = utils.getEtherScanLinkIfExists( etherToken.address, @@ -136,10 +155,13 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt </TableRowColumn> <TableRowColumn> <EthWethConversionButton + refetchEthTokenStateAsync={this._refetchEthTokenStateAsync.bind(this)} + lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} + userAddress={this.props.userAddress} + networkId={this.props.networkId} isOutdatedWrappedEther={false} direction={Side.Deposit} ethToken={etherToken} - ethTokenState={etherTokenState} dispatcher={this.props.dispatcher} blockchain={this.props.blockchain} userEtherBalance={this.props.userEtherBalance} @@ -150,13 +172,23 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt <TableRowColumn className="py1"> {this._renderTokenLink(tokenLabel, etherscanUrl)} </TableRowColumn> - <TableRowColumn>{wethBalance.toFixed(PRECISION)} WETH</TableRowColumn> + <TableRowColumn> + {this.state.isWethStateLoaded ? ( + `${wethBalance.toFixed(PRECISION)} WETH` + ) : ( + <i className="zmdi zmdi-spinner zmdi-hc-spin" /> + )} + </TableRowColumn> <TableRowColumn> <EthWethConversionButton + refetchEthTokenStateAsync={this._refetchEthTokenStateAsync.bind(this)} + lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} + userAddress={this.props.userAddress} + networkId={this.props.networkId} isOutdatedWrappedEther={false} direction={Side.Receive} + isDisabled={!this.state.isWethStateLoaded} ethToken={etherToken} - ethTokenState={etherTokenState} dispatcher={this.props.dispatcher} blockchain={this.props.blockchain} userEtherBalance={this.props.userEtherBalance} @@ -190,7 +222,7 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt </TableRow> </TableHeader> <TableBody displayRowCheckbox={false}> - {this._renderOutdatedWeths(etherToken, etherTokenState)} + {this._renderOutdatedWeths(etherToken, this.state.ethTokenState)} </TableBody> </Table> </div> @@ -269,11 +301,14 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt </TableRowColumn> <TableRowColumn> <EthWethConversionButton + refetchEthTokenStateAsync={this._refetchEthTokenStateAsync.bind(this)} + lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} + userAddress={this.props.userAddress} + networkId={this.props.networkId} isDisabled={!isStateLoaded} isOutdatedWrappedEther={true} direction={Side.Receive} ethToken={outdatedEtherToken} - ethTokenState={outdatedEtherTokenState} dispatcher={this.props.dispatcher} blockchain={this.props.blockchain} userEtherBalance={this.props.userEtherBalance} @@ -338,7 +373,14 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt }, }); } - private async _fetchOutdatedWETHStateAsync() { + private async _fetchWETHStateAsync() { + const tokens = _.values(this.props.tokenByAddress); + const wethToken = _.find(tokens, token => token.symbol === 'WETH'); + const [wethBalance, wethAllowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync( + this.props.userAddress, + wethToken.address, + ); + const outdatedWETHAddresses = this._getOutdatedWETHAddresses(); const outdatedWETHAddressToIsStateLoaded: OutdatedWETHAddressToIsStateLoaded = {}; const outdatedWETHStateByAddress: OutdatedWETHStateByAddress = {}; @@ -353,10 +395,17 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt }; outdatedWETHAddressToIsStateLoaded[address] = true; } - this.setState({ - outdatedWETHStateByAddress, - outdatedWETHAddressToIsStateLoaded, - }); + if (!this._isUnmounted) { + this.setState({ + outdatedWETHStateByAddress, + outdatedWETHAddressToIsStateLoaded, + ethTokenState: { + balance: wethBalance, + allowance: wethAllowance, + }, + isWethStateLoaded: true, + }); + } } private _getOutdatedWETHAddresses(): string[] { const outdatedWETHAddresses = _.compact( @@ -371,4 +420,22 @@ export class EthWrappers extends React.Component<EthWrappersProps, EthWrappersSt ); return outdatedWETHAddresses; } + private _getEthToken() { + const tokens = _.values(this.props.tokenByAddress); + const etherToken = _.find(tokens, { symbol: 'WETH' }); + return etherToken; + } + private async _refetchEthTokenStateAsync() { + const etherToken = this._getEthToken(); + const [balance, allowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync( + this.props.userAddress, + etherToken.address, + ); + this.setState({ + ethTokenState: { + balance, + allowance, + }, + }); + } } // tslint:disable:max-file-line-count diff --git a/packages/website/ts/components/fill_order.tsx b/packages/website/ts/components/fill_order.tsx index 1a150e9ee..d0cfd2cf5 100644 --- a/packages/website/ts/components/fill_order.tsx +++ b/packages/website/ts/components/fill_order.tsx @@ -19,7 +19,7 @@ import { VisualOrder } from 'ts/components/visual_order'; import { Dispatcher } from 'ts/redux/dispatcher'; import { orderSchema } from 'ts/schemas/order_schema'; import { SchemaValidator } from 'ts/schemas/validator'; -import { AlertTypes, BlockchainErrs, Order, Token, TokenByAddress, TokenStateByAddress, WebsitePaths } from 'ts/types'; +import { AlertTypes, BlockchainErrs, Order, Token, TokenByAddress, WebsitePaths } from 'ts/types'; import { colors } from 'ts/utils/colors'; import { constants } from 'ts/utils/constants'; import { errorReporter } from 'ts/utils/error_reporter'; @@ -33,9 +33,9 @@ interface FillOrderProps { networkId: number; userAddress: string; tokenByAddress: TokenByAddress; - tokenStateByAddress: TokenStateByAddress; initialOrder: Order; dispatcher: Dispatcher; + lastForceTokenStateRefetch: number; } interface FillOrderState { @@ -59,8 +59,10 @@ interface FillOrderState { export class FillOrder extends React.Component<FillOrderProps, FillOrderState> { private _validator: SchemaValidator; + private _isUnmounted: boolean; constructor(props: FillOrderProps) { super(props); + this._isUnmounted = false; this.state = { globalErrMsg: '', didOrderValidationRun: false, @@ -90,6 +92,9 @@ export class FillOrder extends React.Component<FillOrderProps, FillOrderState> { public componentDidMount() { window.scrollTo(0, 0); } + public componentWillUnmount() { + this._isUnmounted = true; + } public render() { return ( <div className="clearfix lg-px4 md-px4 sm-px2" style={{ minHeight: 600 }}> @@ -185,7 +190,6 @@ export class FillOrder extends React.Component<FillOrderProps, FillOrderState> { symbol: takerToken.symbol, }; const fillToken = this.props.tokenByAddress[takerToken.address]; - const fillTokenState = this.props.tokenStateByAddress[takerToken.address]; const makerTokenAddress = this.state.parsedOrder.maker.token.address; const makerToken = this.props.tokenByAddress[makerTokenAddress]; const makerAssetToken = { @@ -249,14 +253,17 @@ export class FillOrder extends React.Component<FillOrderProps, FillOrderState> { {!isUserMaker && ( <div className="clearfix mx-auto relative" style={{ width: 235, height: 108 }}> <TokenAmountInput + blockchain={this.props.blockchain} + userAddress={this.props.userAddress} + networkId={this.props.networkId} label="Fill amount" onChange={this._onFillAmountChange.bind(this)} shouldShowIncompleteErrs={false} token={fillToken} - tokenState={fillTokenState} amount={fillAssetToken.amount} shouldCheckBalance={true} shouldCheckAllowance={true} + lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} /> <div className="absolute sm-hide xs-hide" @@ -454,12 +461,14 @@ export class FillOrder extends React.Component<FillOrderProps, FillOrderState> { if (!_.isEmpty(orderJSON)) { orderJSONErrMsg = 'Submitted order JSON is not valid JSON'; } - this.setState({ - didOrderValidationRun: true, - orderJSON, - orderJSONErrMsg, - parsedOrder, - }); + if (!this._isUnmounted) { + this.setState({ + didOrderValidationRun: true, + orderJSON, + orderJSONErrMsg, + parsedOrder, + }); + } return; } @@ -556,11 +565,8 @@ export class FillOrder extends React.Component<FillOrderProps, FillOrderState> { signedOrder, this.props.orderFillAmount, ); - // After fill completes, let's update the token balances - const makerToken = this.props.tokenByAddress[parsedOrder.maker.token.address]; - const takerToken = this.props.tokenByAddress[parsedOrder.taker.token.address]; - const tokens = [makerToken, takerToken]; - await this.props.blockchain.updateTokenBalancesAndAllowancesAsync(tokens); + // After fill completes, let's force fetch the token balances + this.props.dispatcher.forceTokenStateRefetch(); this.setState({ isFilling: false, didFillOrderSucceed: true, @@ -573,7 +579,7 @@ export class FillOrder extends React.Component<FillOrderProps, FillOrderState> { isFilling: false, }); const errMsg = `${err}`; - if (_.includes(errMsg, 'User denied transaction signature')) { + if (utils.didUserDenyWeb3Request(errMsg)) { return; } globalErrMsg = 'Failed to fill order, please refresh and try again'; @@ -653,7 +659,7 @@ export class FillOrder extends React.Component<FillOrderProps, FillOrderState> { isCancelling: false, }); const errMsg = `${err}`; - if (_.includes(errMsg, 'User denied transaction signature')) { + if (utils.didUserDenyWeb3Request(errMsg)) { return; } globalErrMsg = 'Failed to cancel order, please refresh and try again'; diff --git a/packages/website/ts/components/generate_order/asset_picker.tsx b/packages/website/ts/components/generate_order/asset_picker.tsx index df7d87cfd..69fb32a21 100644 --- a/packages/website/ts/components/generate_order/asset_picker.tsx +++ b/packages/website/ts/components/generate_order/asset_picker.tsx @@ -8,7 +8,7 @@ import { TrackTokenConfirmation } from 'ts/components/track_token_confirmation'; import { TokenIcon } from 'ts/components/ui/token_icon'; import { trackedTokenStorage } from 'ts/local_storage/tracked_token_storage'; import { Dispatcher } from 'ts/redux/dispatcher'; -import { DialogConfigs, Token, TokenByAddress, TokenState, TokenVisibility } from 'ts/types'; +import { DialogConfigs, Token, TokenByAddress, TokenVisibility } from 'ts/types'; const TOKEN_ICON_DIMENSION = 100; const TILE_DIMENSION = 146; @@ -223,10 +223,7 @@ export class AssetPicker extends React.Component<AssetPickerProps, AssetPickerSt assetView: AssetViews.NEW_TOKEN_FORM, }); } - private _onNewTokenSubmitted(newToken: Token, newTokenState: TokenState) { - this.props.dispatcher.updateTokenStateByAddress({ - [newToken.address]: newTokenState, - }); + private _onNewTokenSubmitted(newToken: Token) { trackedTokenStorage.addTrackedTokenToUser(this.props.userAddress, this.props.networkId, newToken); this.props.dispatcher.addTokenToTokenByAddress(newToken); this.setState({ @@ -256,15 +253,6 @@ export class AssetPicker extends React.Component<AssetPickerProps, AssetPickerSt newTokenEntry.isTracked = true; trackedTokenStorage.addTrackedTokenToUser(this.props.userAddress, this.props.networkId, newTokenEntry); - const [balance, allowance] = await this.props.blockchain.getCurrentUserTokenBalanceAndAllowanceAsync( - token.address, - ); - this.props.dispatcher.updateTokenStateByAddress({ - [token.address]: { - balance, - allowance, - }, - }); this.props.dispatcher.updateTokenByAddress([newTokenEntry]); this.setState({ isAddingTokenToTracked: false, diff --git a/packages/website/ts/components/generate_order/generate_order_form.tsx b/packages/website/ts/components/generate_order/generate_order_form.tsx index 3ae0d48a7..df1241d8d 100644 --- a/packages/website/ts/components/generate_order/generate_order_form.tsx +++ b/packages/website/ts/components/generate_order/generate_order_form.tsx @@ -27,7 +27,6 @@ import { SignatureData, Token, TokenByAddress, - TokenStateByAddress, } from 'ts/types'; import { colors } from 'ts/utils/colors'; import { errorReporter } from 'ts/utils/error_reporter'; @@ -53,7 +52,7 @@ interface GenerateOrderFormProps { orderSalt: BigNumber; sideToAssetToken: SideToAssetToken; tokenByAddress: TokenByAddress; - tokenStateByAddress: TokenStateByAddress; + lastForceTokenStateRefetch: number; } interface GenerateOrderFormState { @@ -80,10 +79,8 @@ export class GenerateOrderForm extends React.Component<GenerateOrderFormProps, G const dispatcher = this.props.dispatcher; const depositTokenAddress = this.props.sideToAssetToken[Side.Deposit].address; const depositToken = this.props.tokenByAddress[depositTokenAddress]; - const depositTokenState = this.props.tokenStateByAddress[depositTokenAddress]; const receiveTokenAddress = this.props.sideToAssetToken[Side.Receive].address; const receiveToken = this.props.tokenByAddress[receiveTokenAddress]; - const receiveTokenState = this.props.tokenStateByAddress[receiveTokenAddress]; const takerExplanation = 'If a taker is specified, only they are<br> \ allowed to fill this order. If no taker is<br> \ @@ -110,9 +107,12 @@ export class GenerateOrderForm extends React.Component<GenerateOrderFormProps, G tokenByAddress={this.props.tokenByAddress} /> <TokenAmountInput + lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} + blockchain={this.props.blockchain} + userAddress={this.props.userAddress} + networkId={this.props.networkId} label="Sell amount" token={depositToken} - tokenState={depositTokenState} amount={this.props.sideToAssetToken[Side.Deposit].amount} onChange={this._onTokenAmountChange.bind(this, depositToken, Side.Deposit)} shouldShowIncompleteErrs={this.state.shouldShowIncompleteErrs} @@ -139,9 +139,12 @@ export class GenerateOrderForm extends React.Component<GenerateOrderFormProps, G tokenByAddress={this.props.tokenByAddress} /> <TokenAmountInput + lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} + blockchain={this.props.blockchain} + userAddress={this.props.userAddress} + networkId={this.props.networkId} label="Receive amount" token={receiveToken} - tokenState={receiveTokenState} amount={this.props.sideToAssetToken[Side.Receive].amount} onChange={this._onTokenAmountChange.bind(this, receiveToken, Side.Receive)} shouldShowIncompleteErrs={this.state.shouldShowIncompleteErrs} @@ -242,8 +245,10 @@ export class GenerateOrderForm extends React.Component<GenerateOrderFormProps, G // Check if all required inputs were supplied const debitToken = this.props.sideToAssetToken[Side.Deposit]; - const debitBalance = this.props.tokenStateByAddress[debitToken.address].balance; - const debitAllowance = this.props.tokenStateByAddress[debitToken.address].allowance; + const [debitBalance, debitAllowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync( + this.props.userAddress, + debitToken.address, + ); const receiveAmount = this.props.sideToAssetToken[Side.Receive].amount; if ( !_.isUndefined(debitToken.amount) && diff --git a/packages/website/ts/components/generate_order/new_token_form.tsx b/packages/website/ts/components/generate_order/new_token_form.tsx index 63645be9a..f76830a49 100644 --- a/packages/website/ts/components/generate_order/new_token_form.tsx +++ b/packages/website/ts/components/generate_order/new_token_form.tsx @@ -1,4 +1,3 @@ -import { BigNumber } from '@0xproject/utils'; import * as _ from 'lodash'; import TextField from 'material-ui/TextField'; import * as React from 'react'; @@ -7,13 +6,13 @@ import { AddressInput } from 'ts/components/inputs/address_input'; import { Alert } from 'ts/components/ui/alert'; import { LifeCycleRaisedButton } from 'ts/components/ui/lifecycle_raised_button'; import { RequiredLabel } from 'ts/components/ui/required_label'; -import { AlertTypes, Token, TokenByAddress, TokenState } from 'ts/types'; +import { AlertTypes, Token, TokenByAddress } from 'ts/types'; import { colors } from 'ts/utils/colors'; interface NewTokenFormProps { blockchain: Blockchain; tokenByAddress: TokenByAddress; - onNewTokenSubmitted: (token: Token, tokenState: TokenState) => void; + onNewTokenSubmitted: (token: Token) => void; } interface NewTokenFormState { @@ -110,13 +109,9 @@ export class NewTokenForm extends React.Component<NewTokenFormProps, NewTokenFor } let hasBalanceAllowanceErr = false; - let balance = new BigNumber(0); - let allowance = new BigNumber(0); if (doesContractExist) { try { - [balance, allowance] = await this.props.blockchain.getCurrentUserTokenBalanceAndAllowanceAsync( - this.state.address, - ); + await this.props.blockchain.getCurrentUserTokenBalanceAndAllowanceAsync(this.state.address); } catch (err) { hasBalanceAllowanceErr = true; } @@ -155,11 +150,7 @@ export class NewTokenForm extends React.Component<NewTokenFormProps, NewTokenFor isTracked: true, isRegistered: false, }; - const newTokenState: TokenState = { - balance, - allowance, - }; - this.props.onNewTokenSubmitted(newToken, newTokenState); + this.props.onNewTokenSubmitted(newToken); } private _onTokenNameChanged(e: any, name: string) { let nameErrText = ''; diff --git a/packages/website/ts/components/inputs/allowance_toggle.tsx b/packages/website/ts/components/inputs/allowance_toggle.tsx index da46db4f4..45531e74b 100644 --- a/packages/website/ts/components/inputs/allowance_toggle.tsx +++ b/packages/website/ts/components/inputs/allowance_toggle.tsx @@ -17,6 +17,8 @@ interface AllowanceToggleProps { token: Token; tokenState: TokenState; userAddress: string; + isDisabled: boolean; + refetchTokenStateAsync: () => Promise<void>; } interface AllowanceToggleState { @@ -45,7 +47,7 @@ export class AllowanceToggle extends React.Component<AllowanceToggleProps, Allow <div className="flex"> <div> <Toggle - disabled={this.state.isSpinnerVisible} + disabled={this.state.isSpinnerVisible || this.props.isDisabled} toggled={this._isAllowanceSet()} onToggle={this._onToggleAllowanceAsync.bind(this)} /> @@ -73,12 +75,13 @@ export class AllowanceToggle extends React.Component<AllowanceToggleProps, Allow } try { await this.props.blockchain.setProxyAllowanceAsync(this.props.token, newAllowanceAmountInBaseUnits); + await this.props.refetchTokenStateAsync(); } catch (err) { this.setState({ isSpinnerVisible: false, }); const errMsg = `${err}`; - if (_.includes(errMsg, 'User denied transaction')) { + if (utils.didUserDenyWeb3Request(errMsg)) { return; } utils.consoleLog(`Unexpected error encountered: ${err}`); diff --git a/packages/website/ts/components/inputs/balance_bounded_input.tsx b/packages/website/ts/components/inputs/balance_bounded_input.tsx index ddc434b51..3bbc7a5f6 100644 --- a/packages/website/ts/components/inputs/balance_bounded_input.tsx +++ b/packages/website/ts/components/inputs/balance_bounded_input.tsx @@ -18,6 +18,7 @@ interface BalanceBoundedInputProps { validate?: (amount: BigNumber) => InputErrMsg; onVisitBalancesPageClick?: () => void; shouldHideVisitBalancesLink?: boolean; + isDisabled?: boolean; } interface BalanceBoundedInputState { @@ -29,6 +30,7 @@ export class BalanceBoundedInput extends React.Component<BalanceBoundedInputProp public static defaultProps: Partial<BalanceBoundedInputProps> = { shouldShowIncompleteErrs: false, shouldHideVisitBalancesLink: false, + isDisabled: false, }; constructor(props: BalanceBoundedInputProps) { super(props); @@ -88,6 +90,7 @@ export class BalanceBoundedInput extends React.Component<BalanceBoundedInputProp hintText={<span style={{ textTransform: 'capitalize' }}>amount</span>} onChange={this._onValueChange.bind(this)} underlineStyle={{ width: 'calc(100% + 50px)' }} + disabled={this.props.isDisabled} /> ); } @@ -100,7 +103,7 @@ export class BalanceBoundedInput extends React.Component<BalanceBoundedInputProp }, () => { const isValid = _.isUndefined(errMsg); - if (utils.isNumeric(amountString)) { + if (utils.isNumeric(amountString) && !_.includes(amountString, '-')) { this.props.onChange(isValid, new BigNumber(amountString)); } else { this.props.onChange(isValid); diff --git a/packages/website/ts/components/inputs/token_amount_input.tsx b/packages/website/ts/components/inputs/token_amount_input.tsx index 63966d759..2b167d875 100644 --- a/packages/website/ts/components/inputs/token_amount_input.tsx +++ b/packages/website/ts/components/inputs/token_amount_input.tsx @@ -3,13 +3,16 @@ import { BigNumber } from '@0xproject/utils'; import * as _ from 'lodash'; import * as React from 'react'; import { Link } from 'react-router-dom'; +import { Blockchain } from 'ts/blockchain'; import { BalanceBoundedInput } from 'ts/components/inputs/balance_bounded_input'; -import { InputErrMsg, Token, TokenState, ValidatedBigNumberCallback, WebsitePaths } from 'ts/types'; +import { InputErrMsg, Token, ValidatedBigNumberCallback, WebsitePaths } from 'ts/types'; import { colors } from 'ts/utils/colors'; interface TokenAmountInputProps { + userAddress: string; + networkId: number; + blockchain: Blockchain; token: Token; - tokenState: TokenState; label?: string; amount?: BigNumber; shouldShowIncompleteErrs: boolean; @@ -17,11 +20,45 @@ interface TokenAmountInputProps { shouldCheckAllowance: boolean; onChange: ValidatedBigNumberCallback; onVisitBalancesPageClick?: () => void; + lastForceTokenStateRefetch: number; } -interface TokenAmountInputState {} +interface TokenAmountInputState { + balance: BigNumber; + allowance: BigNumber; + isBalanceAndAllowanceLoaded: boolean; +} export class TokenAmountInput extends React.Component<TokenAmountInputProps, TokenAmountInputState> { + private _isUnmounted: boolean; + constructor(props: TokenAmountInputProps) { + super(props); + this._isUnmounted = false; + const defaultAmount = new BigNumber(0); + this.state = { + balance: defaultAmount, + allowance: defaultAmount, + isBalanceAndAllowanceLoaded: false, + }; + } + public componentWillMount() { + // tslint:disable-next-line:no-floating-promises + this._fetchBalanceAndAllowanceAsync(this.props.token.address, this.props.userAddress); + } + public componentWillUnmount() { + this._isUnmounted = true; + } + public componentWillReceiveProps(nextProps: TokenAmountInputProps) { + if ( + nextProps.userAddress !== this.props.userAddress || + nextProps.networkId !== this.props.networkId || + nextProps.token.address !== this.props.token.address || + nextProps.lastForceTokenStateRefetch !== this.props.lastForceTokenStateRefetch + ) { + // tslint:disable-next-line:no-floating-promises + this._fetchBalanceAndAllowanceAsync(nextProps.token.address, nextProps.userAddress); + } + } public render() { const amount = this.props.amount ? ZeroEx.toUnitAmount(this.props.amount, this.props.token.decimals) @@ -32,12 +69,13 @@ export class TokenAmountInput extends React.Component<TokenAmountInputProps, Tok <BalanceBoundedInput label={this.props.label} amount={amount} - balance={ZeroEx.toUnitAmount(this.props.tokenState.balance, this.props.token.decimals)} + balance={ZeroEx.toUnitAmount(this.state.balance, this.props.token.decimals)} onChange={this._onChange.bind(this)} validate={this._validate.bind(this)} shouldCheckBalance={this.props.shouldCheckBalance} shouldShowIncompleteErrs={this.props.shouldShowIncompleteErrs} onVisitBalancesPageClick={this.props.onVisitBalancesPageClick} + isDisabled={!this.state.isBalanceAndAllowanceLoaded} /> <div style={{ paddingTop: hasLabel ? 39 : 14 }}>{this.props.token.symbol}</div> </div> @@ -51,7 +89,7 @@ export class TokenAmountInput extends React.Component<TokenAmountInputProps, Tok this.props.onChange(isValid, baseUnitAmount); } private _validate(amount: BigNumber): InputErrMsg { - if (this.props.shouldCheckAllowance && amount.gt(this.props.tokenState.allowance)) { + if (this.props.shouldCheckAllowance && amount.gt(this.state.allowance)) { return ( <span> Insufficient allowance.{' '} @@ -67,4 +105,20 @@ export class TokenAmountInput extends React.Component<TokenAmountInputProps, Tok return undefined; } } + private async _fetchBalanceAndAllowanceAsync(tokenAddress: string, userAddress: string) { + this.setState({ + isBalanceAndAllowanceLoaded: false, + }); + const [balance, allowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync( + userAddress, + tokenAddress, + ); + if (!this._isUnmounted) { + this.setState({ + balance, + allowance, + isBalanceAndAllowanceLoaded: true, + }); + } + } } diff --git a/packages/website/ts/components/portal.tsx b/packages/website/ts/components/portal.tsx index e2e28e8b6..92589f75c 100644 --- a/packages/website/ts/components/portal.tsx +++ b/packages/website/ts/components/portal.tsx @@ -1,11 +1,13 @@ import { BigNumber } from '@0xproject/utils'; import * as _ from 'lodash'; +import CircularProgress from 'material-ui/CircularProgress'; import Paper from 'material-ui/Paper'; import * as React from 'react'; import * as DocumentTitle from 'react-document-title'; import { Route, Switch } from 'react-router-dom'; import { Blockchain } from 'ts/blockchain'; import { BlockchainErrDialog } from 'ts/components/dialogs/blockchain_err_dialog'; +import { LedgerConfigDialog } from 'ts/components/dialogs/ledger_config_dialog'; import { PortalDisclaimerDialog } from 'ts/components/dialogs/portal_disclaimer_dialog'; import { WrappedEthSectionNoticeDialog } from 'ts/components/dialogs/wrapped_eth_section_notice_dialog'; import { EthWrappers } from 'ts/components/eth_wrappers'; @@ -13,25 +15,15 @@ import { FillOrder } from 'ts/components/fill_order'; import { Footer } from 'ts/components/footer'; import { PortalMenu } from 'ts/components/portal_menu'; import { TokenBalances } from 'ts/components/token_balances'; -import { TopBar } from 'ts/components/top_bar'; +import { TopBar } from 'ts/components/top_bar/top_bar'; import { TradeHistory } from 'ts/components/trade_history/trade_history'; import { FlashMessage } from 'ts/components/ui/flash_message'; -import { Loading } from 'ts/components/ui/loading'; import { GenerateOrderForm } from 'ts/containers/generate_order_form'; import { localStorage } from 'ts/local_storage/local_storage'; import { Dispatcher } from 'ts/redux/dispatcher'; import { orderSchema } from 'ts/schemas/order_schema'; import { SchemaValidator } from 'ts/schemas/validator'; -import { - BlockchainErrs, - HashData, - Order, - ScreenWidths, - Token, - TokenByAddress, - TokenStateByAddress, - WebsitePaths, -} from 'ts/types'; +import { BlockchainErrs, HashData, Order, ProviderType, ScreenWidths, TokenByAddress, WebsitePaths } from 'ts/types'; import { colors } from 'ts/utils/colors'; import { configs } from 'ts/utils/configs'; import { constants } from 'ts/utils/constants'; @@ -46,18 +38,20 @@ export interface PortalAllProps { blockchainIsLoaded: boolean; dispatcher: Dispatcher; hashData: HashData; + injectedProviderName: string; networkId: number; nodeVersion: string; orderFillAmount: BigNumber; + providerType: ProviderType; screenWidth: ScreenWidths; tokenByAddress: TokenByAddress; - tokenStateByAddress: TokenStateByAddress; userEtherBalance: BigNumber; userAddress: string; shouldBlockchainErrDialogBeOpen: boolean; userSuppliedOrderCache: Order; location: Location; flashMessage?: string | React.ReactNode; + lastForceTokenStateRefetch: number; } interface PortalAllState { @@ -67,6 +61,7 @@ interface PortalAllState { prevPathname: string; isDisclaimerDialogOpen: boolean; isWethNoticeDialogOpen: boolean; + isLedgerDialogOpen: boolean; } export class Portal extends React.Component<PortalAllProps, PortalAllState> { @@ -96,6 +91,7 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> { prevPathname: this.props.location.pathname, isDisclaimerDialogOpen: !hasAcceptedDisclaimer, isWethNoticeDialogOpen: !hasAlreadyDismissedWethNotice && isViewingBalances, + isLedgerDialogOpen: false, }; } public componentDidMount() { @@ -125,11 +121,6 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> { if (nextProps.userAddress !== this.state.prevUserAddress) { // tslint:disable-next-line:no-floating-promises this._blockchain.userAddressUpdatedFireAndForgetAsync(nextProps.userAddress); - if (!_.isEmpty(nextProps.userAddress) && nextProps.blockchainIsLoaded) { - const tokens = _.values(nextProps.tokenByAddress); - // tslint:disable-next-line:no-floating-promises - this._updateBalanceAndAllowanceWithLoadingScreenAsync(tokens); - } this.setState({ prevUserAddress: nextProps.userAddress, }); @@ -167,8 +158,14 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> { <DocumentTitle title="0x Portal DApp" /> <TopBar userAddress={this.props.userAddress} + networkId={this.props.networkId} + injectedProviderName={this.props.injectedProviderName} + onToggleLedgerDialog={this.onToggleLedgerDialog.bind(this)} + dispatcher={this.props.dispatcher} + providerType={this.props.providerType} blockchainIsLoaded={this.props.blockchainIsLoaded} location={this.props.location} + blockchain={this._blockchain} /> <div id="portal" className="mx-auto max-width-4" style={{ width: '100%' }}> <Paper className="mb3 mt2"> @@ -215,7 +212,19 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> { /> </Switch> ) : ( - <Loading /> + <div className="pt4 sm-px2 sm-pt2 sm-m1" style={{ height: 500 }}> + <div + className="relative sm-px2 sm-pt2 sm-m1" + style={{ height: 122, top: '50%', transform: 'translateY(-50%)' }} + > + <div className="center pb2"> + <CircularProgress size={40} thickness={5} /> + </div> + <div className="center pt2" style={{ paddingBottom: 11 }}> + Loading Portal... + </div> + </div> + </div> )} </div> </div> @@ -239,11 +248,26 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> { onToggleDialog={this._onPortalDisclaimerAccepted.bind(this)} /> <FlashMessage dispatcher={this.props.dispatcher} flashMessage={this.props.flashMessage} /> + {this.props.blockchainIsLoaded && ( + <LedgerConfigDialog + providerType={this.props.providerType} + networkId={this.props.networkId} + blockchain={this._blockchain} + dispatcher={this.props.dispatcher} + toggleDialogFn={this.onToggleLedgerDialog.bind(this)} + isOpen={this.state.isLedgerDialogOpen} + /> + )} </div> - <Footer /> + <Footer />; </div> ); } + public onToggleLedgerDialog() { + this.setState({ + isLedgerDialogOpen: !this.state.isLedgerDialogOpen, + }); + } private _renderEthWrapper() { return ( <EthWrappers @@ -251,9 +275,9 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> { blockchain={this._blockchain} dispatcher={this.props.dispatcher} tokenByAddress={this.props.tokenByAddress} - tokenStateByAddress={this.props.tokenStateByAddress} userAddress={this.props.userAddress} userEtherBalance={this.props.userEtherBalance} + lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} /> ); } @@ -267,6 +291,8 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> { ); } private _renderTokenBalances() { + const allTokens = _.values(this.props.tokenByAddress); + const trackedTokens = _.filter(allTokens, t => t.isTracked); return ( <TokenBalances blockchain={this._blockchain} @@ -275,10 +301,11 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> { dispatcher={this.props.dispatcher} screenWidth={this.props.screenWidth} tokenByAddress={this.props.tokenByAddress} - tokenStateByAddress={this.props.tokenStateByAddress} + trackedTokens={trackedTokens} userAddress={this.props.userAddress} userEtherBalance={this.props.userEtherBalance} networkId={this.props.networkId} + lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} /> ); } @@ -296,8 +323,8 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> { networkId={this.props.networkId} userAddress={this.props.userAddress} tokenByAddress={this.props.tokenByAddress} - tokenStateByAddress={this.props.tokenStateByAddress} dispatcher={this.props.dispatcher} + lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} /> ); } @@ -353,9 +380,4 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> { const newScreenWidth = utils.getScreenWidth(); this.props.dispatcher.updateScreenWidth(newScreenWidth); } - private async _updateBalanceAndAllowanceWithLoadingScreenAsync(tokens: Token[]) { - this.props.dispatcher.updateBlockchainIsLoaded(false); - await this._blockchain.updateTokenBalancesAndAllowancesAsync(tokens); - this.props.dispatcher.updateBlockchainIsLoaded(true); - } } diff --git a/packages/website/ts/components/send_button.tsx b/packages/website/ts/components/send_button.tsx index f94ec346a..ffa165f60 100644 --- a/packages/website/ts/components/send_button.tsx +++ b/packages/website/ts/components/send_button.tsx @@ -5,16 +5,19 @@ import * as React from 'react'; import { Blockchain } from 'ts/blockchain'; import { SendDialog } from 'ts/components/dialogs/send_dialog'; import { Dispatcher } from 'ts/redux/dispatcher'; -import { BlockchainCallErrs, Token, TokenState } from 'ts/types'; +import { BlockchainCallErrs, Token } from 'ts/types'; import { errorReporter } from 'ts/utils/error_reporter'; import { utils } from 'ts/utils/utils'; interface SendButtonProps { + userAddress: string; + networkId: number; token: Token; - tokenState: TokenState; dispatcher: Dispatcher; blockchain: Blockchain; onError: () => void; + lastForceTokenStateRefetch: number; + refetchTokenStateAsync: (tokenAddress: string) => Promise<void>; } interface SendButtonState { @@ -42,11 +45,14 @@ export class SendButton extends React.Component<SendButtonProps, SendButtonState onClick={this._toggleSendDialog.bind(this)} /> <SendDialog + blockchain={this.props.blockchain} + userAddress={this.props.userAddress} + networkId={this.props.networkId} isOpen={this.state.isSendDialogVisible} onComplete={this._onSendAmountSelectedAsync.bind(this)} onCancelled={this._toggleSendDialog.bind(this)} token={this.props.token} - tokenState={this.props.tokenState} + lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} /> </div> ); @@ -62,18 +68,15 @@ export class SendButton extends React.Component<SendButtonProps, SendButtonState }); this._toggleSendDialog(); const token = this.props.token; - const tokenState = this.props.tokenState; - let balance = tokenState.balance; try { await this.props.blockchain.transferAsync(token, recipient, value); - balance = balance.minus(value); - this.props.dispatcher.replaceTokenBalanceByAddress(token.address, balance); + await this.props.refetchTokenStateAsync(token.address); } catch (err) { const errMsg = `${err}`; if (_.includes(errMsg, BlockchainCallErrs.UserHasNoAssociatedAddresses)) { this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true); return; - } else if (!_.includes(errMsg, 'User denied transaction')) { + } else if (!utils.didUserDenyWeb3Request(errMsg)) { utils.consoleLog(`Unexpected error encountered: ${err}`); utils.consoleLog(err.stack); this.props.onError(); diff --git a/packages/website/ts/components/token_balances.tsx b/packages/website/ts/components/token_balances.tsx index 2cef413c7..c6a9a46be 100644 --- a/packages/website/ts/components/token_balances.tsx +++ b/packages/website/ts/components/token_balances.tsx @@ -27,11 +27,11 @@ import { BlockchainCallErrs, BlockchainErrs, EtherscanLinkSuffixes, + Networks, ScreenWidths, Styles, Token, TokenByAddress, - TokenStateByAddress, TokenVisibility, } from 'ts/types'; import { colors } from 'ts/utils/colors'; @@ -58,6 +58,14 @@ const styles: Styles = { }, }; +interface TokenStateByAddress { + [address: string]: { + balance: BigNumber; + allowance: BigNumber; + isLoaded: boolean; + }; +} + interface TokenBalancesProps { blockchain: Blockchain; blockchainErr: BlockchainErrs; @@ -65,10 +73,11 @@ interface TokenBalancesProps { dispatcher: Dispatcher; screenWidth: ScreenWidths; tokenByAddress: TokenByAddress; - tokenStateByAddress: TokenStateByAddress; + trackedTokens: Token[]; userAddress: string; userEtherBalance: BigNumber; networkId: number; + lastForceTokenStateRefetch: number; } interface TokenBalancesState { @@ -76,14 +85,17 @@ interface TokenBalancesState { isBalanceSpinnerVisible: boolean; isDharmaDialogVisible: boolean; isZRXSpinnerVisible: boolean; - currentZrxBalance?: BigNumber; isTokenPickerOpen: boolean; isAddingToken: boolean; + trackedTokenStateByAddress: TokenStateByAddress; } export class TokenBalances extends React.Component<TokenBalancesProps, TokenBalancesState> { + private _isUnmounted: boolean; public constructor(props: TokenBalancesProps) { super(props); + this._isUnmounted = false; + const initialTrackedTokenStateByAddress = this._getInitialTrackedTokenStateByAddress(props.trackedTokens); this.state = { errorType: undefined, isBalanceSpinnerVisible: false, @@ -91,8 +103,17 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala isDharmaDialogVisible: DharmaLoanFrame.isAuthTokenPresent(), isTokenPickerOpen: false, isAddingToken: false, + trackedTokenStateByAddress: initialTrackedTokenStateByAddress, }; } + public componentWillMount() { + const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress); + // tslint:disable-next-line:no-floating-promises + this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses); + } + public componentWillUnmount() { + this._isUnmounted = true; + } public componentWillReceiveProps(nextProps: TokenBalancesProps) { if (nextProps.userEtherBalance !== this.props.userEtherBalance) { if (this.state.isBalanceSpinnerVisible) { @@ -103,18 +124,36 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala 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`); + + if ( + nextProps.userAddress !== this.props.userAddress || + nextProps.networkId !== this.props.networkId || + nextProps.lastForceTokenStateRefetch !== this.props.lastForceTokenStateRefetch + ) { + const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress); + // tslint:disable-next-line:no-floating-promises + this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses); + } + + if (!_.isEqual(nextProps.trackedTokens, this.props.trackedTokens)) { + const newTokens = _.difference(nextProps.trackedTokens, this.props.trackedTokens); + const newTokenAddresses = _.map(newTokens, token => token.address); + // Add placeholder entry for this token to the state, since fetching the + // balance/allowance is asynchronous + const trackedTokenStateByAddress = this.state.trackedTokenStateByAddress; + for (const tokenAddress of newTokenAddresses) { + trackedTokenStateByAddress[tokenAddress] = { + balance: new BigNumber(0), + allowance: new BigNumber(0), + isLoaded: false, + }; } this.setState({ - isZRXSpinnerVisible: false, - currentZrxBalance: undefined, + trackedTokenStateByAddress, }); + // Fetch the actual balance/allowance. + // tslint:disable-next-line:no-floating-promises + this._fetchBalancesAndAllowancesAsync(newTokenAddresses); } } public componentDidMount() { @@ -137,13 +176,13 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala onTouchTap={this._onDharmaDialogToggle.bind(this, false)} />, ]; - const isTestNetwork = this.props.networkId === constants.NETWORK_ID_TESTNET; + const isKovanTestNetwork = this.props.networkId === constants.NETWORK_ID_KOVAN; const dharmaButtonColumnStyle = { paddingLeft: 3, - display: isTestNetwork ? 'table-cell' : 'none', + display: isKovanTestNetwork ? 'table-cell' : 'none', }; const stubColumnStyle = { - display: isTestNetwork ? 'none' : 'table-cell', + display: isKovanTestNetwork ? 'none' : 'table-cell', }; const allTokenRowHeight = _.size(this.props.tokenByAddress) * TOKEN_TABLE_ROW_HEIGHT; const tokenTableHeight = @@ -162,10 +201,10 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala 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> + <h3>{isKovanTestNetwork ? 'Test ether' : 'Ether'}</h3> <Divider /> <div className="pt2 pb2"> - {isTestNetwork + {isKovanTestNetwork ? '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. \ @@ -177,12 +216,12 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala <TableHeaderColumn>Currency</TableHeaderColumn> <TableHeaderColumn>Balance</TableHeaderColumn> <TableRowColumn className="sm-hide xs-hide" style={stubColumnStyle} /> - {isTestNetwork && ( + {isKovanTestNetwork && ( <TableHeaderColumn style={{ paddingLeft: 3 }}> {isSmallScreen ? 'Faucet' : 'Request from faucet'} </TableHeaderColumn> )} - {isTestNetwork && ( + {isKovanTestNetwork && ( <TableHeaderColumn style={dharmaButtonColumnStyle}> {isSmallScreen ? 'Loan' : 'Request Dharma loan'} <HelpTooltip style={{ paddingLeft: 4 }} explanation={dharmaLoanExplanation} /> @@ -204,7 +243,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala )} </TableRowColumn> <TableRowColumn className="sm-hide xs-hide" style={stubColumnStyle} /> - {isTestNetwork && ( + {isKovanTestNetwork && ( <TableRowColumn style={{ paddingLeft: 3 }}> <LifeCycleRaisedButton labelReady="Request" @@ -214,7 +253,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala /> </TableRowColumn> )} - {isTestNetwork && ( + {isKovanTestNetwork && ( <TableRowColumn style={dharmaButtonColumnStyle}> <RaisedButton label="Request" @@ -228,7 +267,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala </Table> <div className="clearfix" style={{ paddingBottom: 1 }}> <div className="col col-10"> - <h3 className="pt2">{isTestNetwork ? 'Test tokens' : 'Tokens'}</h3> + <h3 className="pt2">{isKovanTestNetwork ? 'Test tokens' : 'Tokens'}</h3> </div> <div className="col col-1 pt3 align-right"> <FloatingActionButton mini={true} zDepth={0} onClick={this._onAddTokenClicked.bind(this)}> @@ -243,7 +282,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala </div> <Divider /> <div className="pt2 pb2"> - {isTestNetwork + {isKovanTestNetwork ? "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> @@ -303,8 +342,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala 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 trackedTokens = this.props.trackedTokens; const trackedTokensStartingWithEtherToken = trackedTokens.sort( firstBy((t: Token) => t.symbol !== ETHER_TOKEN_SYMBOL) .thenBy((t: Token) => t.symbol !== ZRX_TOKEN_SYMBOL) @@ -317,7 +355,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala return tableRows; } private _renderTokenRow(tokenColSpan: number, actionPaddingX: number, token: Token) { - const tokenState = this.props.tokenStateByAddress[token.address]; + const tokenState = this.state.trackedTokenStateByAddress[token.address]; const tokenLink = utils.getEtherScanLinkIfExists( token.address, this.props.networkId, @@ -338,13 +376,19 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala )} </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> - )} + {tokenState.isLoaded ? ( + <span> + {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> + )} + </span> + ) : ( + <i className="zmdi zmdi-spinner zmdi-hc-spin" /> + )} </TableRowColumn> <TableRowColumn> <AllowanceToggle @@ -354,6 +398,8 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala tokenState={tokenState} onErrorOccurred={this._onErrorOccurred.bind(this)} userAddress={this.props.userAddress} + isDisabled={!tokenState.isLoaded} + refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this, token.address)} /> </TableRowColumn> <TableRowColumn style={{ paddingLeft: actionPaddingX, paddingRight: actionPaddingX }}> @@ -366,7 +412,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala /> )} {token.symbol === ZRX_TOKEN_SYMBOL && - this.props.networkId === constants.NETWORK_ID_TESTNET && ( + this.props.networkId === constants.NETWORK_ID_KOVAN && ( <LifeCycleRaisedButton labelReady="Request" labelLoading="Sending..." @@ -383,11 +429,14 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala }} > <SendButton + userAddress={this.props.userAddress} + networkId={this.props.networkId} blockchain={this.props.blockchain} dispatcher={this.props.dispatcher} token={token} - tokenState={tokenState} onError={this._onSendFailed.bind(this)} + lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} + refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this, token.address)} /> </TableRowColumn> )} @@ -414,7 +463,6 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala } 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`); @@ -449,9 +497,9 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala 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. + Our faucet can only send test Ether to addresses on the {Networks.Kovan} testnet (networkId{' '} + {constants.NETWORK_ID_KOVAN}). Please make sure you are connected to the {Networks.Kovan}{' '} + testnet and try requesting ether again. </div> ); @@ -510,6 +558,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala private async _onMintTestTokensAsync(token: Token): Promise<boolean> { try { await this.props.blockchain.mintTestTokensAsync(token); + await this._refetchTokenStateAsync(token.address); const amount = ZeroEx.toUnitAmount(constants.MINT_AMOUNT, token.decimals); this.props.dispatcher.showFlashMessage(`Successfully minted ${amount.toString(10)} ${token.symbol}`); return true; @@ -519,7 +568,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true); return false; } - if (_.includes(errMsg, 'User denied transaction')) { + if (utils.didUserDenyWeb3Request(errMsg)) { return false; } utils.consoleLog(`Unexpected error encountered: ${err}`); @@ -539,7 +588,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala // 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) { + if (this.props.blockchain.networkId !== constants.NETWORK_ID_KOVAN) { this.setState({ errorType: BalanceErrs.incorrectNetworkForFaucet, }); @@ -569,15 +618,11 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala 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); + this._startPollingZrxBalanceAsync(); } return true; } @@ -603,4 +648,65 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala isAddingToken: false, }); } + private async _startPollingZrxBalanceAsync() { + const tokens = _.values(this.props.tokenByAddress); + const zrxToken = _.find(tokens, t => t.symbol === ZRX_TOKEN_SYMBOL); + + // tslint:disable-next-line:no-floating-promises + const balance = await this.props.blockchain.pollTokenBalanceAsync(zrxToken); + const trackedTokenStateByAddress = this.state.trackedTokenStateByAddress; + trackedTokenStateByAddress[zrxToken.address] = { + ...trackedTokenStateByAddress[zrxToken.address], + balance, + }; + this.setState({ + isZRXSpinnerVisible: false, + }); + } + private async _fetchBalancesAndAllowancesAsync(tokenAddresses: string[]) { + const trackedTokenStateByAddress = this.state.trackedTokenStateByAddress; + for (const tokenAddress of tokenAddresses) { + const [balance, allowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync( + this.props.userAddress, + tokenAddress, + ); + trackedTokenStateByAddress[tokenAddress] = { + balance, + allowance, + isLoaded: true, + }; + } + if (!this._isUnmounted) { + this.setState({ + trackedTokenStateByAddress, + }); + } + } + private _getInitialTrackedTokenStateByAddress(trackedTokens: Token[]) { + const trackedTokenStateByAddress: TokenStateByAddress = {}; + _.each(trackedTokens, token => { + trackedTokenStateByAddress[token.address] = { + balance: new BigNumber(0), + allowance: new BigNumber(0), + isLoaded: false, + }; + }); + return trackedTokenStateByAddress; + } + private async _refetchTokenStateAsync(tokenAddress: string) { + const [balance, allowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync( + this.props.userAddress, + tokenAddress, + ); + this.setState({ + trackedTokenStateByAddress: { + ...this.state.trackedTokenStateByAddress, + [tokenAddress]: { + balance, + allowance, + isLoaded: true, + }, + }, + }); + } } // tslint:disable:max-file-line-count diff --git a/packages/website/ts/components/top_bar/provider_display.tsx b/packages/website/ts/components/top_bar/provider_display.tsx new file mode 100644 index 000000000..39e7f2a8c --- /dev/null +++ b/packages/website/ts/components/top_bar/provider_display.tsx @@ -0,0 +1,148 @@ +import * as _ from 'lodash'; +import RaisedButton from 'material-ui/RaisedButton'; +import * as React from 'react'; +import { Blockchain } from 'ts/blockchain'; +import { ProviderPicker } from 'ts/components/top_bar/provider_picker'; +import { DropDown } from 'ts/components/ui/drop_down'; +import { Identicon } from 'ts/components/ui/identicon'; +import { Dispatcher } from 'ts/redux/dispatcher'; +import { ProviderType } from 'ts/types'; +import { colors } from 'ts/utils/colors'; +import { constants } from 'ts/utils/constants'; +import { utils } from 'ts/utils/utils'; + +const IDENTICON_DIAMETER = 32; + +interface ProviderDisplayProps { + dispatcher: Dispatcher; + userAddress: string; + networkId: number; + injectedProviderName: string; + providerType: ProviderType; + onToggleLedgerDialog: () => void; + blockchain: Blockchain; +} + +interface ProviderDisplayState {} + +export class ProviderDisplay extends React.Component<ProviderDisplayProps, ProviderDisplayState> { + public render() { + const isAddressAvailable = !_.isEmpty(this.props.userAddress); + const isExternallyInjectedProvider = ProviderType.Injected && this.props.injectedProviderName !== '0x Public'; + const displayAddress = isAddressAvailable + ? utils.getAddressBeginAndEnd(this.props.userAddress) + : isExternallyInjectedProvider ? 'Account locked' : '0x0000...0000'; + // If the "injected" provider is our fallback public node, then we want to + // show the "connect a wallet" message instead of the providerName + const injectedProviderName = isExternallyInjectedProvider + ? this.props.injectedProviderName + : 'Connect a wallet'; + const providerTitle = + this.props.providerType === ProviderType.Injected ? injectedProviderName : 'Ledger Nano S'; + const hoverActiveNode = ( + <div className="flex right lg-pr0 md-pr2 sm-pr2" style={{ paddingTop: 16 }}> + <div> + <Identicon address={this.props.userAddress} diameter={IDENTICON_DIAMETER} /> + </div> + <div style={{ marginLeft: 12, paddingTop: 1 }}> + <div style={{ fontSize: 12, color: colors.amber800 }}>{providerTitle}</div> + <div style={{ fontSize: 14 }}>{displayAddress}</div> + </div> + <div + style={{ borderLeft: `1px solid ${colors.grey300}`, marginLeft: 17, paddingTop: 1 }} + className="px2" + > + <i style={{ fontSize: 30, color: colors.grey300 }} className="zmdi zmdi zmdi-chevron-down" /> + </div> + </div> + ); + const hasInjectedProvider = + this.props.injectedProviderName !== '0x Public' && this.props.providerType === ProviderType.Injected; + const hasLedgerProvider = this.props.providerType === ProviderType.Ledger; + const horizontalPosition = hasInjectedProvider || hasLedgerProvider ? 'left' : 'middle'; + return ( + <div style={{ width: 'fit-content', height: 48, float: 'right' }}> + <DropDown + hoverActiveNode={hoverActiveNode} + popoverContent={this.renderPopoverContent(hasInjectedProvider, hasLedgerProvider)} + anchorOrigin={{ horizontal: horizontalPosition, vertical: 'bottom' }} + targetOrigin={{ horizontal: horizontalPosition, vertical: 'top' }} + zDepth={1} + /> + </div> + ); + } + public renderPopoverContent(hasInjectedProvider: boolean, hasLedgerProvider: boolean) { + if (hasInjectedProvider || hasLedgerProvider) { + return ( + <ProviderPicker + dispatcher={this.props.dispatcher} + networkId={this.props.networkId} + injectedProviderName={this.props.injectedProviderName} + providerType={this.props.providerType} + onToggleLedgerDialog={this.props.onToggleLedgerDialog} + blockchain={this.props.blockchain} + /> + ); + } else { + // Nothing to connect to, show install/info popover + return ( + <div className="px2" style={{ maxWidth: 420 }}> + <div className="center h4 py2" style={{ color: colors.grey700 }}> + Choose a wallet: + </div> + <div className="flex pb3"> + <div className="center px2"> + <div style={{ color: colors.darkGrey }}>Install a browser wallet</div> + <div className="py2"> + <img src="/images/metamask_or_parity.png" width="135" /> + </div> + <div> + Use{' '} + <a + href={constants.URL_METAMASK_CHROME_STORE} + target="_blank" + style={{ color: colors.lightBlueA700 }} + > + Metamask + </a>{' '} + or{' '} + <a + href={constants.URL_PARITY_CHROME_STORE} + target="_blank" + style={{ color: colors.lightBlueA700 }} + > + Parity Signer + </a> + </div> + </div> + <div> + <div + className="pl1 ml1" + style={{ borderLeft: `1px solid ${colors.grey300}`, height: 65 }} + /> + <div className="py1">or</div> + <div + className="pl1 ml1" + style={{ borderLeft: `1px solid ${colors.grey300}`, height: 68 }} + /> + </div> + <div className="px2 center"> + <div style={{ color: colors.darkGrey }}>Connect to a ledger hardware wallet</div> + <div style={{ paddingTop: 21, paddingBottom: 29 }}> + <img src="/images/ledger_icon.png" style={{ width: 80 }} /> + </div> + <div> + <RaisedButton + style={{ width: '100%' }} + label="Use Ledger" + onClick={this.props.onToggleLedgerDialog} + /> + </div> + </div> + </div> + </div> + ); + } + } +} diff --git a/packages/website/ts/components/top_bar/provider_picker.tsx b/packages/website/ts/components/top_bar/provider_picker.tsx new file mode 100644 index 000000000..be7e57d6f --- /dev/null +++ b/packages/website/ts/components/top_bar/provider_picker.tsx @@ -0,0 +1,81 @@ +import * as _ from 'lodash'; +import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton'; +import * as React from 'react'; +import { Blockchain } from 'ts/blockchain'; +import { Dispatcher } from 'ts/redux/dispatcher'; +import { ProviderType } from 'ts/types'; +import { colors } from 'ts/utils/colors'; +import { constants } from 'ts/utils/constants'; + +interface ProviderPickerProps { + networkId: number; + injectedProviderName: string; + providerType: ProviderType; + onToggleLedgerDialog: () => void; + dispatcher: Dispatcher; + blockchain: Blockchain; +} + +interface ProviderPickerState {} + +export class ProviderPicker extends React.Component<ProviderPickerProps, ProviderPickerState> { + public render() { + const isLedgerSelected = this.props.providerType === ProviderType.Ledger; + const menuStyle = { + padding: 10, + paddingTop: 15, + paddingBottom: 15, + }; + // Show dropdown with two options + return ( + <div style={{ width: 225, overflow: 'hidden' }}> + <RadioButtonGroup name="provider" defaultSelected={this.props.providerType}> + <RadioButton + onClick={this._onProviderRadioChanged.bind(this, ProviderType.Injected)} + style={{ ...menuStyle, backgroundColor: !isLedgerSelected && colors.grey50 }} + value={ProviderType.Injected} + label={this._renderLabel(this.props.injectedProviderName, !isLedgerSelected)} + /> + <RadioButton + onClick={this._onProviderRadioChanged.bind(this, ProviderType.Ledger)} + style={{ ...menuStyle, backgroundColor: isLedgerSelected && colors.grey50 }} + value={ProviderType.Ledger} + label={this._renderLabel('Ledger Nano S', isLedgerSelected)} + /> + </RadioButtonGroup> + </div> + ); + } + private _renderLabel(title: string, shouldShowNetwork: boolean) { + const label = ( + <div className="flex"> + <div style={{ fontSize: 14 }}>{title}</div> + {shouldShowNetwork && this._renderNetwork()} + </div> + ); + return label; + } + private _renderNetwork() { + const networkName = constants.NETWORK_NAME_BY_ID[this.props.networkId]; + return ( + <div className="flex" style={{ marginTop: 1 }}> + <div className="relative" style={{ width: 14, paddingLeft: 14 }}> + <img + src={`/images/network_icons/${networkName.toLowerCase()}.png`} + className="absolute" + style={{ top: 6, width: 10 }} + /> + </div> + <div style={{ color: colors.lightGrey, fontSize: 11 }}>{networkName}</div> + </div> + ); + } + private _onProviderRadioChanged(value: string) { + if (value === ProviderType.Ledger) { + this.props.onToggleLedgerDialog(); + } else { + // tslint:disable-next-line:no-floating-promises + this.props.blockchain.updateProviderToInjectedAsync(); + } + } +} diff --git a/packages/website/ts/components/top_bar.tsx b/packages/website/ts/components/top_bar/top_bar.tsx index 11d3e7cc2..1a0691e83 100644 --- a/packages/website/ts/components/top_bar.tsx +++ b/packages/website/ts/components/top_bar/top_bar.tsx @@ -1,21 +1,31 @@ import * as _ from 'lodash'; import Drawer from 'material-ui/Drawer'; +import Menu from 'material-ui/Menu'; import MenuItem from 'material-ui/MenuItem'; import * as React from 'react'; import { Link } from 'react-router-dom'; import ReactTooltip = require('react-tooltip'); +import { Blockchain } from 'ts/blockchain'; import { PortalMenu } from 'ts/components/portal_menu'; -import { TopBarMenuItem } from 'ts/components/top_bar_menu_item'; -import { DropDownMenuItem } from 'ts/components/ui/drop_down_menu_item'; +import { ProviderDisplay } from 'ts/components/top_bar/provider_display'; +import { TopBarMenuItem } from 'ts/components/top_bar/top_bar_menu_item'; +import { DropDown } from 'ts/components/ui/drop_down'; import { Identicon } from 'ts/components/ui/identicon'; import { DocsInfo } from 'ts/pages/documentation/docs_info'; import { NestedSidebarMenu } from 'ts/pages/shared/nested_sidebar_menu'; -import { DocsMenu, MenuSubsectionsBySection, Styles, WebsitePaths } from 'ts/types'; +import { Dispatcher } from 'ts/redux/dispatcher'; +import { DocsMenu, MenuSubsectionsBySection, ProviderType, Styles, WebsitePaths } from 'ts/types'; import { colors } from 'ts/utils/colors'; import { constants } from 'ts/utils/constants'; interface TopBarProps { userAddress?: string; + networkId?: number; + injectedProviderName?: string; + providerType?: ProviderType; + onToggleLedgerDialog?: () => void; + blockchain?: Blockchain; + dispatcher?: Dispatcher; blockchainIsLoaded: boolean; location: Location; docsVersion?: string; @@ -125,6 +135,15 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> { cursor: 'pointer', paddingTop: 16, }; + const hoverActiveNode = ( + <div className="flex relative" style={{ color: menuIconStyle.color }}> + <div style={{ paddingRight: 10 }}>Developers</div> + <div className="absolute" style={{ paddingLeft: 3, right: 3, top: -2 }}> + <i className="zmdi zmdi-caret-right" style={{ fontSize: 22 }} /> + </div> + </div> + ); + const popoverContent = <Menu style={{ color: colors.darkGrey }}>{developerSectionMenuItems}</Menu>; return ( <div style={{ ...styles.topBar, ...bottomBorderStyle, ...this.props.style }} className="pb1"> <div className={parentClassNames}> @@ -138,11 +157,12 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> { {!this._isViewingPortal() && ( <div className={menuClasses}> <div className="flex justify-between"> - <DropDownMenuItem - title="Developers" - subMenuItems={developerSectionMenuItems} + <DropDown + hoverActiveNode={hoverActiveNode} + popoverContent={popoverContent} + anchorOrigin={{ horizontal: 'middle', vertical: 'bottom' }} + targetOrigin={{ horizontal: 'middle', vertical: 'top' }} style={styles.menuItem} - isNightVersion={isNightVersion} /> <TopBarMenuItem title="Wiki" @@ -167,10 +187,19 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> { </div> </div> )} - {this.props.blockchainIsLoaded && - !_.isEmpty(this.props.userAddress) && ( - <div className="col col-5 sm-hide xs-hide">{this._renderUser()}</div> - )} + {this.props.blockchainIsLoaded && ( + <div className="sm-hide xs-hide col col-5"> + <ProviderDisplay + dispatcher={this.props.dispatcher} + userAddress={this.props.userAddress} + networkId={this.props.networkId} + injectedProviderName={this.props.injectedProviderName} + providerType={this.props.providerType} + onToggleLedgerDialog={this.props.onToggleLedgerDialog} + blockchain={this.props.blockchain} + /> + </div> + )} <div className={`col ${isFullWidthPage ? 'col-2 pl2' : 'col-1'} md-hide lg-hide`}> <div style={menuIconStyle}> <i className="zmdi zmdi-menu" onClick={this._onMenuButtonClick.bind(this)} /> diff --git a/packages/website/ts/components/top_bar_menu_item.tsx b/packages/website/ts/components/top_bar/top_bar_menu_item.tsx index 96ee86142..96ee86142 100644 --- a/packages/website/ts/components/top_bar_menu_item.tsx +++ b/packages/website/ts/components/top_bar/top_bar_menu_item.tsx diff --git a/packages/website/ts/components/ui/drop_down_menu_item.tsx b/packages/website/ts/components/ui/drop_down.tsx index a578fb4f9..63b9eec0b 100644 --- a/packages/website/ts/components/ui/drop_down_menu_item.tsx +++ b/packages/website/ts/components/ui/drop_down.tsx @@ -1,36 +1,35 @@ import * as _ from 'lodash'; -import Menu from 'material-ui/Menu'; -import Popover from 'material-ui/Popover'; +import Popover, { PopoverAnimationVertical } from 'material-ui/Popover'; import * as React from 'react'; -import { colors } from 'ts/utils/colors'; +import { MaterialUIPosition } from 'ts/types'; const CHECK_CLOSE_POPOVER_INTERVAL_MS = 300; const DEFAULT_STYLE = { fontSize: 14, }; -interface DropDownMenuItemProps { - title: string; - subMenuItems: React.ReactNode[]; +interface DropDownProps { + hoverActiveNode: React.ReactNode; + popoverContent: React.ReactNode; + anchorOrigin: MaterialUIPosition; + targetOrigin: MaterialUIPosition; style?: React.CSSProperties; - menuItemStyle?: React.CSSProperties; - isNightVersion?: boolean; + zDepth?: number; } -interface DropDownMenuItemState { +interface DropDownState { isDropDownOpen: boolean; anchorEl?: HTMLInputElement; } -export class DropDownMenuItem extends React.Component<DropDownMenuItemProps, DropDownMenuItemState> { - public static defaultProps: Partial<DropDownMenuItemProps> = { +export class DropDown extends React.Component<DropDownProps, DropDownState> { + public static defaultProps: Partial<DropDownProps> = { style: DEFAULT_STYLE, - menuItemStyle: DEFAULT_STYLE, - isNightVersion: false, + zDepth: 1, }; private _isHovering: boolean; private _popoverCloseCheckIntervalId: number; - constructor(props: DropDownMenuItemProps) { + constructor(props: DropDownProps) { super(props); this.state = { isDropDownOpen: false, @@ -44,30 +43,35 @@ export class DropDownMenuItem extends React.Component<DropDownMenuItemProps, Dro public componentWillUnmount() { window.clearInterval(this._popoverCloseCheckIntervalId); } + public componentWillReceiveProps(nextProps: DropDownProps) { + // HACK: If the popoverContent is updated to a different dimension and the users + // mouse is no longer above it, the dropdown can enter an inconsistent state where + // it believes the user is still hovering over it. In order to remedy this, we + // call hoverOff whenever the dropdown receives updated props. This is a hack + // because it will effectively close the dropdown on any prop update, barring + // dropdowns from having dynamic content. + this._onHoverOff(); + } public render() { - const colorStyle = this.props.isNightVersion ? 'white' : this.props.style.color; return ( <div - style={{ ...this.props.style, color: colorStyle }} + style={{ ...this.props.style, width: 'fit-content', height: '100%' }} onMouseEnter={this._onHover.bind(this)} onMouseLeave={this._onHoverOff.bind(this)} > - <div className="flex relative"> - <div style={{ paddingRight: 10 }}>{this.props.title}</div> - <div className="absolute" style={{ paddingLeft: 3, right: 3, top: -2 }}> - <i className="zmdi zmdi-caret-right" style={{ fontSize: 22 }} /> - </div> - </div> + {this.props.hoverActiveNode} <Popover open={this.state.isDropDownOpen} anchorEl={this.state.anchorEl} - anchorOrigin={{ horizontal: 'middle', vertical: 'bottom' }} - targetOrigin={{ horizontal: 'middle', vertical: 'top' }} + anchorOrigin={this.props.anchorOrigin} + targetOrigin={this.props.targetOrigin} onRequestClose={this._closePopover.bind(this)} useLayerForClickAway={false} + animation={PopoverAnimationVertical} + zDepth={this.props.zDepth} > <div onMouseEnter={this._onHover.bind(this)} onMouseLeave={this._onHoverOff.bind(this)}> - <Menu style={{ color: colors.grey }}>{this.props.subMenuItems}</Menu> + {this.props.popoverContent} </div> </Popover> </div> @@ -87,7 +91,7 @@ export class DropDownMenuItem extends React.Component<DropDownMenuItemProps, Dro anchorEl: event.currentTarget, }); } - private _onHoverOff(event: React.FormEvent<HTMLInputElement>) { + private _onHoverOff() { this._isHovering = false; } private _checkIfShouldClosePopover() { diff --git a/packages/website/ts/components/ui/loading.tsx b/packages/website/ts/components/ui/loading.tsx deleted file mode 100644 index aa319e9e9..000000000 --- a/packages/website/ts/components/ui/loading.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import * as _ from 'lodash'; -import Paper from 'material-ui/Paper'; -import * as React from 'react'; -import { DefaultPlayer as Video } from 'react-html5video'; -import 'react-html5video/dist/styles.css'; -import { utils } from 'ts/utils/utils'; - -interface LoadingProps {} - -interface LoadingState {} - -export class Loading extends React.Component<LoadingProps, LoadingState> { - public render() { - return ( - <div className="pt4 sm-px2 sm-pt2 sm-m1" style={{ height: 500 }}> - <Paper className="mx-auto" style={{ maxWidth: 400 }}> - {utils.isUserOnMobile() ? ( - <img className="p1" src="/gifs/0xAnimation.gif" width="96%" /> - ) : ( - <div style={{ pointerEvents: 'none' }}> - <Video - autoPlay={true} - loop={true} - muted={true} - controls={[]} - poster="/images/loading_poster.png" - > - <source src="/videos/0xAnimation.mp4" type="video/mp4" /> - </Video> - </div> - )} - <div className="center pt2" style={{ paddingBottom: 11 }}> - Connecting to the blockchain... - </div> - </Paper> - </div> - ); - } -} |