aboutsummaryrefslogtreecommitdiffstats
path: root/packages/website/ts/components/dialogs
diff options
context:
space:
mode:
authorFabio Berger <me@fabioberger.com>2017-11-22 04:03:08 +0800
committerFabio Berger <me@fabioberger.com>2017-11-22 04:03:08 +0800
commit3660ba28d73d70d08bf14c33ef680e5ef3ec7f3b (patch)
treef101656799da807489253e17bea7abfaea90b62d /packages/website/ts/components/dialogs
parent037f466e1f80f635b48f3235258402e2ce75fb7b (diff)
downloaddexon-0x-contracts-3660ba28d73d70d08bf14c33ef680e5ef3ec7f3b.tar.gz
dexon-0x-contracts-3660ba28d73d70d08bf14c33ef680e5ef3ec7f3b.tar.zst
dexon-0x-contracts-3660ba28d73d70d08bf14c33ef680e5ef3ec7f3b.zip
Add website to mono repo, update packages to align with existing sub-packages, use new subscribeAsync 0x.js method
Diffstat (limited to 'packages/website/ts/components/dialogs')
-rw-r--r--packages/website/ts/components/dialogs/blockchain_err_dialog.tsx158
-rw-r--r--packages/website/ts/components/dialogs/eth_weth_conversion_dialog.tsx139
-rw-r--r--packages/website/ts/components/dialogs/ledger_config_dialog.tsx288
-rw-r--r--packages/website/ts/components/dialogs/portal_disclaimer_dialog.tsx44
-rw-r--r--packages/website/ts/components/dialogs/send_dialog.tsx126
-rw-r--r--packages/website/ts/components/dialogs/track_token_confirmation_dialog.tsx99
-rw-r--r--packages/website/ts/components/dialogs/u2f_not_supported_dialog.tsx53
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>
+ );
+}