diff options
Diffstat (limited to 'packages/website/ts/components/dialogs')
7 files changed, 907 insertions, 0 deletions
diff --git a/packages/website/ts/components/dialogs/blockchain_err_dialog.tsx b/packages/website/ts/components/dialogs/blockchain_err_dialog.tsx new file mode 100644 index 000000000..2e12fc889 --- /dev/null +++ b/packages/website/ts/components/dialogs/blockchain_err_dialog.tsx @@ -0,0 +1,158 @@ +import * as _ from 'lodash'; +import * as React from 'react'; +import Dialog from 'material-ui/Dialog'; +import FlatButton from 'material-ui/FlatButton'; +import {colors} from 'material-ui/styles'; +import {constants} from 'ts/utils/constants'; +import {configs} from 'ts/utils/configs'; +import {Blockchain} from 'ts/blockchain'; +import {BlockchainErrs} from 'ts/types'; + +interface BlockchainErrDialogProps { + blockchain: Blockchain; + blockchainErr: BlockchainErrs; + isOpen: boolean; + userAddress: string; + toggleDialogFn: (isOpen: boolean) => void; + networkId: number; +} + +export class BlockchainErrDialog extends React.Component<BlockchainErrDialogProps, undefined> { + public render() { + const dialogActions = [ + <FlatButton + label="Ok" + primary={true} + onTouchTap={this.props.toggleDialogFn.bind(this.props.toggleDialogFn, false)} + />, + ]; + + const hasWalletAddress = this.props.userAddress !== ''; + return ( + <Dialog + title={this.getTitle(hasWalletAddress)} + titleStyle={{fontWeight: 100}} + actions={dialogActions} + open={this.props.isOpen} + contentStyle={{width: 400}} + onRequestClose={this.props.toggleDialogFn.bind(this.props.toggleDialogFn, false)} + autoScrollBodyContent={true} + > + <div className="pt2" style={{color: colors.grey700}}> + {this.renderExplanation(hasWalletAddress)} + </div> + </Dialog> + ); + } + private getTitle(hasWalletAddress: boolean) { + if (this.props.blockchainErr === BlockchainErrs.A_CONTRACT_NOT_DEPLOYED_ON_NETWORK) { + return '0x smart contracts not found'; + } else if (!hasWalletAddress) { + return 'Enable wallet communication'; + } else if (this.props.blockchainErr === BlockchainErrs.DISCONNECTED_FROM_ETHEREUM_NODE) { + return 'Disconnected from Ethereum network'; + } else { + return 'Unexpected error'; + } + } + private renderExplanation(hasWalletAddress: boolean) { + if (this.props.blockchainErr === BlockchainErrs.A_CONTRACT_NOT_DEPLOYED_ON_NETWORK) { + return this.renderContractsNotDeployedExplanation(); + } else if (!hasWalletAddress) { + return this.renderNoWalletFoundExplanation(); + } else if (this.props.blockchainErr === BlockchainErrs.DISCONNECTED_FROM_ETHEREUM_NODE) { + return this.renderDisconnectedFromNode(); + } else { + return this.renderUnexpectedErrorExplanation(); + } + } + private renderDisconnectedFromNode() { + return ( + <div> + You were disconnected from the backing Ethereum node. + {' '}If using <a href={constants.METAMASK_CHROME_STORE_URL} target="_blank"> + Metamask + </a> or <a href={constants.MIST_DOWNLOAD_URL} target="_blank">Mist</a> try refreshing + {' '}the page. If using a locally hosted Ethereum node, make sure it's still running. + </div> + ); + } + private renderUnexpectedErrorExplanation() { + return ( + <div> + We encountered an unexpected error. Please try refreshing the page. + </div> + ); + } + private renderNoWalletFoundExplanation() { + return ( + <div> + <div> + We were unable to access an Ethereum wallet you control. In order to interact + {' '}with the 0x portal dApp, + we need a way to interact with one of your Ethereum wallets. + {' '}There are two easy ways you can enable us to do that: + </div> + <h4>1. Metamask chrome extension</h4> + <div> + You can install the{' '} + <a href={constants.METAMASK_CHROME_STORE_URL} target="_blank"> + Metamask + </a> Chrome extension Ethereum wallet. Once installed and set up, refresh this page. + <div className="pt1"> + <span className="bold">Note:</span> + {' '}If you already have Metamask installed, make sure it is unlocked. + </div> + </div> + <h4>Parity Signer</h4> + <div> + The <a href={constants.PARITY_CHROME_STORE_URL} target="_blank">Parity Signer + Chrome extension</a>{' '}lets you connect to a locally running Parity node. + Make sure you have started your local Parity node with{' '} + {configs.isMainnetEnabled && '`parity ui` or'} `parity --chain kovan ui`{' '} + in order to connect to {configs.isMainnetEnabled ? 'mainnet or Kovan respectively.' : 'Kovan.'} + </div> + <div className="pt2"> + <span className="bold">Note:</span> + {' '}If you have done one of the above steps and are still seeing this message, + {' '}we might still be unable to retrieve an Ethereum address by calling `web3.eth.accounts`. + {' '}Make sure you have created at least one Ethereum address. + </div> + </div> + ); + } + private renderContractsNotDeployedExplanation() { + return ( + <div> + <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.TESTNET_NETWORK_ID}) + {configs.isMainnetEnabled ? + ` or ${constants.MAINNET_NAME} (network Id: ${constants.MAINNET_NETWORK_ID}).` : + `.` + } + </div> + <h4>Metamask</h4> + <div> + If you are using{' '} + <a href={constants.METAMASK_CHROME_STORE_URL} target="_blank"> + Metamask + </a>, you can switch networks in the top left corner of the extension popover. + </div> + <h4>Parity Signer</h4> + <div> + If using the <a href={constants.PARITY_CHROME_STORE_URL} target="_blank">Parity Signer + Chrome extension</a>, make sure to start your local Parity node with{' '} + {configs.isMainnetEnabled ? + '`parity ui` or `parity --chain Kovan ui` in order to connect to mainnet \ + or Kovan respectively.' : + '`parity --chain kovan ui` in order to connect to Kovan.' + } + </div> + </div> + ); + } +} diff --git a/packages/website/ts/components/dialogs/eth_weth_conversion_dialog.tsx b/packages/website/ts/components/dialogs/eth_weth_conversion_dialog.tsx new file mode 100644 index 000000000..1db85e375 --- /dev/null +++ b/packages/website/ts/components/dialogs/eth_weth_conversion_dialog.tsx @@ -0,0 +1,139 @@ +import * as React from 'react'; +import Dialog from 'material-ui/Dialog'; +import FlatButton from 'material-ui/FlatButton'; +import RadioButtonGroup from 'material-ui/RadioButton/RadioButtonGroup'; +import RadioButton from 'material-ui/RadioButton'; +import {Side, Token, TokenState} from 'ts/types'; +import {TokenAmountInput} from 'ts/components/inputs/token_amount_input'; +import {EthAmountInput} from 'ts/components/inputs/eth_amount_input'; +import BigNumber from 'bignumber.js'; + +interface EthWethConversionDialogProps { + onComplete: (direction: Side, value: BigNumber) => void; + onCancelled: () => void; + isOpen: boolean; + token: Token; + tokenState: TokenState; + etherBalance: BigNumber; +} + +interface EthWethConversionDialogState { + value?: BigNumber; + direction: Side; + shouldShowIncompleteErrs: boolean; + hasErrors: boolean; +} + +export class EthWethConversionDialog extends + React.Component<EthWethConversionDialogProps, EthWethConversionDialogState> { + constructor() { + super(); + this.state = { + direction: Side.deposit, + shouldShowIncompleteErrs: false, + hasErrors: true, + }; + } + public render() { + const convertDialogActions = [ + <FlatButton + key="cancel" + label="Cancel" + onTouchTap={this.onCancel.bind(this)} + />, + <FlatButton + key="convert" + label="Convert" + primary={true} + onTouchTap={this.onConvertClick.bind(this)} + />, + ]; + return ( + <Dialog + title="I want to convert" + titleStyle={{fontWeight: 100}} + actions={convertDialogActions} + open={this.props.isOpen} + > + {this.renderConversionDialogBody()} + </Dialog> + ); + } + private renderConversionDialogBody() { + return ( + <div className="mx-auto" style={{maxWidth: 300}}> + <RadioButtonGroup + className="pb1" + defaultSelected={this.state.direction} + name="conversionDirection" + onChange={this.onConversionDirectionChange.bind(this)} + > + <RadioButton + className="pb1" + value={Side.deposit} + label="Ether -> Ether Tokens" + /> + <RadioButton + value={Side.receive} + label="Ether Tokens -> Ether" + /> + </RadioButtonGroup> + {this.state.direction === Side.receive ? + <TokenAmountInput + label="Amount to convert" + 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} + /> : + <EthAmountInput + label="Amount to convert" + balance={this.props.etherBalance} + amount={this.state.value} + onChange={this.onValueChange.bind(this)} + shouldCheckBalance={true} + shouldShowIncompleteErrs={this.state.shouldShowIncompleteErrs} + onVisitBalancesPageClick={this.props.onCancelled} + /> + } + </div> + ); + } + private onConversionDirectionChange(e: any, direction: Side) { + this.setState({ + value: undefined, + shouldShowIncompleteErrs: false, + direction, + hasErrors: true, + }); + } + private onValueChange(isValid: boolean, amount?: BigNumber) { + this.setState({ + value: amount, + hasErrors: !isValid, + }); + } + private onConvertClick() { + if (this.state.hasErrors) { + this.setState({ + shouldShowIncompleteErrs: true, + }); + } else { + const value = this.state.value; + this.setState({ + value: undefined, + }); + this.props.onComplete(this.state.direction, value); + } + } + private onCancel() { + this.setState({ + value: undefined, + }); + this.props.onCancelled(); + } +} diff --git a/packages/website/ts/components/dialogs/ledger_config_dialog.tsx b/packages/website/ts/components/dialogs/ledger_config_dialog.tsx new file mode 100644 index 000000000..f89935500 --- /dev/null +++ b/packages/website/ts/components/dialogs/ledger_config_dialog.tsx @@ -0,0 +1,288 @@ +import * as _ from 'lodash'; +import * as React from 'react'; +import BigNumber from 'bignumber.js'; +import {colors} from 'material-ui/styles'; +import Dialog from 'material-ui/Dialog'; +import FlatButton from 'material-ui/FlatButton'; +import TextField from 'material-ui/TextField'; +import { + Table, + TableBody, + TableHeader, + TableRow, + TableHeaderColumn, + TableRowColumn, +} from 'material-ui/Table'; +import ReactTooltip = require('react-tooltip'); +import {utils} from 'ts/utils/utils'; +import {constants} from 'ts/utils/constants'; +import {Blockchain} from 'ts/blockchain'; +import {Dispatcher} from 'ts/redux/dispatcher'; +import {LifeCycleRaisedButton} from 'ts/components/ui/lifecycle_raised_button'; + +const VALID_ETHEREUM_DERIVATION_PATH_PREFIX = `44'/60'`; + +enum LedgerSteps { + CONNECT, + SELECT_ADDRESS, +} + +interface LedgerConfigDialogProps { + isOpen: boolean; + toggleDialogFn: (isOpen: boolean) => void; + dispatcher: Dispatcher; + blockchain: Blockchain; + networkId: number; +} + +interface LedgerConfigDialogState { + didConnectFail: boolean; + stepIndex: LedgerSteps; + userAddresses: string[]; + addressBalances: BigNumber[]; + derivationPath: string; + derivationErrMsg: string; +} + +export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps, LedgerConfigDialogState> { + constructor(props: LedgerConfigDialogProps) { + super(props); + this.state = { + didConnectFail: false, + stepIndex: LedgerSteps.CONNECT, + userAddresses: [], + addressBalances: [], + derivationPath: constants.DEFAULT_DERIVATION_PATH, + derivationErrMsg: '', + }; + } + public render() { + const dialogActions = [ + <FlatButton + label="Cancel" + onTouchTap={this.onClose.bind(this)} + />, + ]; + const dialogTitle = this.state.stepIndex === LedgerSteps.CONNECT ? + 'Connect to your Ledger' : + 'Select desired address'; + return ( + <Dialog + title={dialogTitle} + titleStyle={{fontWeight: 100}} + actions={dialogActions} + open={this.props.isOpen} + onRequestClose={this.onClose.bind(this)} + autoScrollBodyContent={true} + bodyStyle={{paddingBottom: 0}} + > + <div style={{color: colors.grey700, paddingTop: 1}}> + {this.state.stepIndex === LedgerSteps.CONNECT && + this.renderConnectStep() + } + {this.state.stepIndex === LedgerSteps.SELECT_ADDRESS && + this.renderSelectAddressStep() + } + </div> + </Dialog> + ); + } + private renderConnectStep() { + return ( + <div> + <div className="h4 pt3"> + Follow these instructions before proceeding: + </div> + <ol> + <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"> + 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> + </ol> + <div className="center pb3"> + <LifeCycleRaisedButton + isPrimary={true} + labelReady="Connect to Ledger" + labelLoading="Connecting..." + labelComplete="Connected!" + onClickAsyncFn={this.onConnectLedgerClickAsync.bind(this, true)} + /> + {this.state.didConnectFail && + <div className="pt2 left-align" style={{color: colors.red200}}> + Failed to connect. Follow the instructions and try again. + </div> + } + </div> + </div> + ); + } + private renderSelectAddressStep() { + return ( + <div> + <div> + <Table + bodyStyle={{height: 300}} + onRowSelection={this.onAddressSelected.bind(this)} + > + <TableHeader displaySelectAll={false}> + <TableRow> + <TableHeaderColumn colSpan={2}>Address</TableHeaderColumn> + <TableHeaderColumn>Balance</TableHeaderColumn> + </TableRow> + </TableHeader> + <TableBody> + {this.renderAddressTableRows()} + </TableBody> + </Table> + </div> + <div className="flex pt2" style={{height: 100}}> + <div className="overflow-hidden" style={{width: 180}}> + <TextField + floatingLabelFixed={true} + floatingLabelStyle={{color: colors.grey500}} + floatingLabelText="Update path derivation (advanced)" + value={this.state.derivationPath} + errorText={this.state.derivationErrMsg} + onChange={this.onDerivationPathChanged.bind(this)} + /> + </div> + <div className="pl2" style={{paddingTop: 28}}> + <LifeCycleRaisedButton + labelReady="Update" + labelLoading="Updating..." + labelComplete="Updated!" + onClickAsyncFn={this.onFetchAddressesForDerivationPathAsync.bind(this, true)} + /> + </div> + </div> + </div> + ); + } + private renderAddressTableRows() { + const rows = _.map(this.state.userAddresses, (userAddress: string, i: number) => { + const balance = this.state.addressBalances[i]; + const addressTooltipId = `address-${userAddress}`; + const balanceTooltipId = `balance-${userAddress}`; + const networkName = constants.networkNameById[this.props.networkId]; + // We specifically prefix kovan ETH. + // TODO: We should probably add prefixes for all networks + const isKovanNetwork = networkName === 'Kovan'; + const balanceString = `${balance.toString()} ${isKovanNetwork ? 'Kovan ' : ''}ETH`; + return ( + <TableRow key={userAddress} style={{height: 40}}> + <TableRowColumn colSpan={2}> + <div + data-tip={true} + data-for={addressTooltipId} + > + {userAddress} + </div> + <ReactTooltip id={addressTooltipId}>{userAddress}</ReactTooltip> + </TableRowColumn> + <TableRowColumn> + <div + data-tip={true} + data-for={balanceTooltipId} + > + {balanceString} + </div> + <ReactTooltip id={balanceTooltipId}>{balanceString}</ReactTooltip> + </TableRowColumn> + </TableRow> + ); + }); + return rows; + } + private onClose() { + this.setState({ + didConnectFail: false, + }); + const isOpen = false; + this.props.toggleDialogFn(isOpen); + } + private onAddressSelected(selectedRowIndexes: number[]) { + const selectedRowIndex = selectedRowIndexes[0]; + this.props.blockchain.updateLedgerDerivationIndex(selectedRowIndex); + const selectedAddress = this.state.userAddresses[selectedRowIndex]; + const selectAddressBalance = this.state.addressBalances[selectedRowIndex]; + this.props.dispatcher.updateUserAddress(selectedAddress); + this.props.blockchain.updateWeb3WrapperPrevUserAddress(selectedAddress); + this.props.dispatcher.updateUserEtherBalance(selectAddressBalance); + this.setState({ + stepIndex: LedgerSteps.CONNECT, + }); + const isOpen = false; + this.props.toggleDialogFn(isOpen); + } + private async onFetchAddressesForDerivationPathAsync() { + const currentlySetPath = this.props.blockchain.getLedgerDerivationPathIfExists(); + if (currentlySetPath === this.state.derivationPath) { + return; + } + this.props.blockchain.updateLedgerDerivationPathIfExists(this.state.derivationPath); + const didSucceed = await this.fetchAddressesAndBalancesAsync(); + if (!didSucceed) { + this.setState({ + derivationErrMsg: 'Failed to connect to Ledger.', + }); + } + return didSucceed; + } + private async fetchAddressesAndBalancesAsync() { + let userAddresses: string[]; + const addressBalances: BigNumber[] = []; + try { + userAddresses = await this.getUserAddressesAsync(); + for (const address of userAddresses) { + const balance = await this.props.blockchain.getBalanceInEthAsync(address); + addressBalances.push(balance); + } + } catch (err) { + utils.consoleLog(`Ledger error: ${JSON.stringify(err)}`); + this.setState({ + didConnectFail: true, + }); + return false; + } + this.setState({ + userAddresses, + addressBalances, + }); + return true; + } + private onDerivationPathChanged(e: any, derivationPath: string) { + let derivationErrMsg = ''; + if (!_.startsWith(derivationPath, VALID_ETHEREUM_DERIVATION_PATH_PREFIX)) { + derivationErrMsg = 'Must be valid Ethereum path.'; + } + + this.setState({ + derivationPath, + derivationErrMsg, + }); + } + private async onConnectLedgerClickAsync() { + const didSucceed = await this.fetchAddressesAndBalancesAsync(); + if (didSucceed) { + this.setState({ + stepIndex: LedgerSteps.SELECT_ADDRESS, + }); + } + return didSucceed; + } + private async getUserAddressesAsync(): Promise<string[]> { + let userAddresses: string[]; + userAddresses = await this.props.blockchain.getUserAccountsAsync(); + + if (_.isEmpty(userAddresses)) { + throw new Error('No addresses retrieved.'); + } + return userAddresses; + } +} diff --git a/packages/website/ts/components/dialogs/portal_disclaimer_dialog.tsx b/packages/website/ts/components/dialogs/portal_disclaimer_dialog.tsx new file mode 100644 index 000000000..8f870b42f --- /dev/null +++ b/packages/website/ts/components/dialogs/portal_disclaimer_dialog.tsx @@ -0,0 +1,44 @@ +import * as React from 'react'; +import {colors} from 'material-ui/styles'; +import FlatButton from 'material-ui/FlatButton'; +import Dialog from 'material-ui/Dialog'; +import {constants} from 'ts/utils/constants'; + +interface PortalDisclaimerDialogProps { + isOpen: boolean; + onToggleDialog: () => void; +} + +export function PortalDisclaimerDialog(props: PortalDisclaimerDialogProps) { + return ( + <Dialog + title="0x Portal Disclaimer" + titleStyle={{fontWeight: 100}} + actions={[ + <FlatButton + label="I Agree" + onTouchTap={props.onToggleDialog.bind(this)} + />, + ]} + open={props.isOpen} + onRequestClose={props.onToggleDialog.bind(this)} + autoScrollBodyContent={true} + modal={true} + > + <div className="pt2" style={{color: colors.grey700}}> + <div> + 0x Portal is a free software-based tool intended to help users to + buy and sell ERC20-compatible blockchain tokens through the 0x protocol + on a purely peer-to-peer basis. 0x portal is not a regulated marketplace, + exchange or intermediary of any kind, and therefore, you should only use + 0x portal to exchange tokens that are not securities, commodity interests, + or any other form of regulated instrument. 0x has not attempted to screen + or otherwise limit the tokens that you may enter in 0x Portal. By clicking + “I Agree” below, you understand that you are solely responsible for using 0x + Portal and buying and selling tokens using 0x Portal in compliance with all + applicable laws and regulations. + </div> + </div> + </Dialog> + ); +} diff --git a/packages/website/ts/components/dialogs/send_dialog.tsx b/packages/website/ts/components/dialogs/send_dialog.tsx new file mode 100644 index 000000000..10417a326 --- /dev/null +++ b/packages/website/ts/components/dialogs/send_dialog.tsx @@ -0,0 +1,126 @@ +import * as React from 'react'; +import * as _ from 'lodash'; +import Dialog from 'material-ui/Dialog'; +import FlatButton from 'material-ui/FlatButton'; +import RadioButtonGroup from 'material-ui/RadioButton/RadioButtonGroup'; +import RadioButton from 'material-ui/RadioButton'; +import {Side, Token, TokenState} from 'ts/types'; +import {TokenAmountInput} from 'ts/components/inputs/token_amount_input'; +import {EthAmountInput} from 'ts/components/inputs/eth_amount_input'; +import {AddressInput} from 'ts/components/inputs/address_input'; +import BigNumber from 'bignumber.js'; + +interface SendDialogProps { + onComplete: (recipient: string, value: BigNumber) => void; + onCancelled: () => void; + isOpen: boolean; + token: Token; + tokenState: TokenState; +} + +interface SendDialogState { + value?: BigNumber; + recipient: string; + shouldShowIncompleteErrs: boolean; + isAmountValid: boolean; +} + +export class SendDialog extends React.Component<SendDialogProps, SendDialogState> { + constructor() { + super(); + this.state = { + recipient: '', + shouldShowIncompleteErrs: false, + isAmountValid: false, + }; + } + public render() { + const transferDialogActions = [ + <FlatButton + key="cancelTransfer" + label="Cancel" + onTouchTap={this.onCancel.bind(this)} + />, + <FlatButton + key="sendTransfer" + disabled={this.hasErrors()} + label="Send" + primary={true} + onTouchTap={this.onSendClick.bind(this)} + />, + ]; + return ( + <Dialog + title="I want to send" + titleStyle={{fontWeight: 100}} + actions={transferDialogActions} + open={this.props.isOpen} + > + {this.renderSendDialogBody()} + </Dialog> + ); + } + private renderSendDialogBody() { + return ( + <div className="mx-auto" style={{maxWidth: 300}}> + <div style={{height: 80}}> + <AddressInput + initialAddress={this.state.recipient} + updateAddress={this.onRecipientChange.bind(this)} + isRequired={true} + label={'Recipient address'} + hintText={'Address'} + /> + </div> + <TokenAmountInput + 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} + /> + </div> + ); + } + private onRecipientChange(recipient?: string) { + this.setState({ + shouldShowIncompleteErrs: false, + recipient, + }); + } + private onValueChange(isValid: boolean, amount?: BigNumber) { + this.setState({ + isAmountValid: isValid, + value: amount, + }); + } + private onSendClick() { + if (this.hasErrors()) { + this.setState({ + shouldShowIncompleteErrs: true, + }); + } else { + const value = this.state.value; + this.setState({ + recipient: undefined, + value: undefined, + }); + this.props.onComplete(this.state.recipient, value); + } + } + private onCancel() { + this.setState({ + value: undefined, + }); + this.props.onCancelled(); + } + private hasErrors() { + return _.isUndefined(this.state.recipient) || + _.isUndefined(this.state.value) || + !this.state.isAmountValid; + } +} diff --git a/packages/website/ts/components/dialogs/track_token_confirmation_dialog.tsx b/packages/website/ts/components/dialogs/track_token_confirmation_dialog.tsx new file mode 100644 index 000000000..97c654656 --- /dev/null +++ b/packages/website/ts/components/dialogs/track_token_confirmation_dialog.tsx @@ -0,0 +1,99 @@ +import * as _ from 'lodash'; +import * as React from 'react'; +import {colors} from 'material-ui/styles'; +import FlatButton from 'material-ui/FlatButton'; +import Dialog from 'material-ui/Dialog'; +import {constants} from 'ts/utils/constants'; +import {Blockchain} from 'ts/blockchain'; +import {Dispatcher} from 'ts/redux/dispatcher'; +import {TrackTokenConfirmation} from 'ts/components/track_token_confirmation'; +import {trackedTokenStorage} from 'ts/local_storage/tracked_token_storage'; +import {Token, TokenByAddress} from 'ts/types'; + +interface TrackTokenConfirmationDialogProps { + tokens: Token[]; + tokenByAddress: TokenByAddress; + isOpen: boolean; + onToggleDialog: (didConfirmTokenTracking: boolean) => void; + dispatcher: Dispatcher; + networkId: number; + blockchain: Blockchain; + userAddress: string; +} + +interface TrackTokenConfirmationDialogState { + isAddingTokenToTracked: boolean; +} + +export class TrackTokenConfirmationDialog extends + React.Component<TrackTokenConfirmationDialogProps, TrackTokenConfirmationDialogState> { + constructor(props: TrackTokenConfirmationDialogProps) { + super(props); + this.state = { + isAddingTokenToTracked: false, + }; + } + public render() { + const tokens = this.props.tokens; + return ( + <Dialog + title="Tracking confirmation" + titleStyle={{fontWeight: 100}} + actions={[ + <FlatButton + label="No" + onTouchTap={this.onTrackConfirmationRespondedAsync.bind(this, false)} + />, + <FlatButton + label="Yes" + onTouchTap={this.onTrackConfirmationRespondedAsync.bind(this, true)} + />, + ]} + open={this.props.isOpen} + onRequestClose={this.props.onToggleDialog.bind(this, false)} + autoScrollBodyContent={true} + > + <div className="pt2"> + <TrackTokenConfirmation + tokens={tokens} + networkId={this.props.networkId} + tokenByAddress={this.props.tokenByAddress} + isAddingTokenToTracked={this.state.isAddingTokenToTracked} + /> + </div> + </Dialog> + ); + } + private async onTrackConfirmationRespondedAsync(didUserAcceptTracking: boolean) { + if (!didUserAcceptTracking) { + this.props.onToggleDialog(didUserAcceptTracking); + return; + } + this.setState({ + isAddingTokenToTracked: true, + }); + for (const token of this.props.tokens) { + const newTokenEntry = _.assign({}, token); + + 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({ + isAddingTokenToTracked: false, + }); + this.props.onToggleDialog(didUserAcceptTracking); + } +} diff --git a/packages/website/ts/components/dialogs/u2f_not_supported_dialog.tsx b/packages/website/ts/components/dialogs/u2f_not_supported_dialog.tsx new file mode 100644 index 000000000..28c24cdbe --- /dev/null +++ b/packages/website/ts/components/dialogs/u2f_not_supported_dialog.tsx @@ -0,0 +1,53 @@ +import * as React from 'react'; +import {colors} from 'material-ui/styles'; +import FlatButton from 'material-ui/FlatButton'; +import Dialog from 'material-ui/Dialog'; +import {constants} from 'ts/utils/constants'; + +interface U2fNotSupportedDialogProps { + isOpen: boolean; + onToggleDialog: () => void; +} + +export function U2fNotSupportedDialog(props: U2fNotSupportedDialogProps) { + return ( + <Dialog + title="U2F Not Supported" + titleStyle={{fontWeight: 100}} + actions={[ + <FlatButton + label="Ok" + onTouchTap={props.onToggleDialog.bind(this)} + />, + ]} + open={props.isOpen} + onRequestClose={props.onToggleDialog.bind(this)} + autoScrollBodyContent={true} + > + <div className="pt2" style={{color: colors.grey700}}> + <div> + It looks like your browser does not support U2F connections + required for us to communicate with your hardware wallet. + Please use a browser that supports U2F connections and try + again. + </div> + <div> + <ul> + <li className="pb1">Chrome version 38 or later</li> + <li className="pb1">Opera version 40 of later</li> + <li> + Firefox with{' '} + <a + href={constants.FIREFOX_U2F_ADDON} + target="_blank" + style={{textDecoration: 'underline'}} + > + this extension + </a>. + </li> + </ul> + </div> + </div> + </Dialog> + ); +} |