From 3660ba28d73d70d08bf14c33ef680e5ef3ec7f3b Mon Sep 17 00:00:00 2001 From: Fabio Berger Date: Tue, 21 Nov 2017 14:03:08 -0600 Subject: Add website to mono repo, update packages to align with existing sub-packages, use new subscribeAsync 0x.js method --- .../components/dialogs/blockchain_err_dialog.tsx | 158 +++++++++++ .../dialogs/eth_weth_conversion_dialog.tsx | 139 ++++++++++ .../ts/components/dialogs/ledger_config_dialog.tsx | 288 +++++++++++++++++++++ .../dialogs/portal_disclaimer_dialog.tsx | 44 ++++ .../website/ts/components/dialogs/send_dialog.tsx | 126 +++++++++ .../dialogs/track_token_confirmation_dialog.tsx | 99 +++++++ .../dialogs/u2f_not_supported_dialog.tsx | 53 ++++ 7 files changed, 907 insertions(+) create mode 100644 packages/website/ts/components/dialogs/blockchain_err_dialog.tsx create mode 100644 packages/website/ts/components/dialogs/eth_weth_conversion_dialog.tsx create mode 100644 packages/website/ts/components/dialogs/ledger_config_dialog.tsx create mode 100644 packages/website/ts/components/dialogs/portal_disclaimer_dialog.tsx create mode 100644 packages/website/ts/components/dialogs/send_dialog.tsx create mode 100644 packages/website/ts/components/dialogs/track_token_confirmation_dialog.tsx create mode 100644 packages/website/ts/components/dialogs/u2f_not_supported_dialog.tsx (limited to 'packages/website/ts/components/dialogs') 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 { + public render() { + const dialogActions = [ + , + ]; + + const hasWalletAddress = this.props.userAddress !== ''; + return ( + +
+ {this.renderExplanation(hasWalletAddress)} +
+
+ ); + } + 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 ( +
+ You were disconnected from the backing Ethereum node. + {' '}If using + Metamask + or Mist try refreshing + {' '}the page. If using a locally hosted Ethereum node, make sure it's still running. +
+ ); + } + private renderUnexpectedErrorExplanation() { + return ( +
+ We encountered an unexpected error. Please try refreshing the page. +
+ ); + } + private renderNoWalletFoundExplanation() { + return ( +
+
+ 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: +
+

1. Metamask chrome extension

+
+ You can install the{' '} + + Metamask + Chrome extension Ethereum wallet. Once installed and set up, refresh this page. +
+ Note: + {' '}If you already have Metamask installed, make sure it is unlocked. +
+
+

Parity Signer

+
+ The Parity Signer + Chrome extension{' '}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.'} +
+
+ Note: + {' '}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. +
+
+ ); + } + private renderContractsNotDeployedExplanation() { + return ( +
+
+ 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}).` : + `.` + } +
+

Metamask

+
+ If you are using{' '} + + Metamask + , you can switch networks in the top left corner of the extension popover. +
+

Parity Signer

+
+ If using the Parity Signer + Chrome extension, 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.' + } +
+
+ ); + } +} 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 { + constructor() { + super(); + this.state = { + direction: Side.deposit, + shouldShowIncompleteErrs: false, + hasErrors: true, + }; + } + public render() { + const convertDialogActions = [ + , + , + ]; + return ( + + {this.renderConversionDialogBody()} + + ); + } + private renderConversionDialogBody() { + return ( +
+ + + + + {this.state.direction === Side.receive ? + : + + } +
+ ); + } + 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 { + constructor(props: LedgerConfigDialogProps) { + super(props); + this.state = { + didConnectFail: false, + stepIndex: LedgerSteps.CONNECT, + userAddresses: [], + addressBalances: [], + derivationPath: constants.DEFAULT_DERIVATION_PATH, + derivationErrMsg: '', + }; + } + public render() { + const dialogActions = [ + , + ]; + const dialogTitle = this.state.stepIndex === LedgerSteps.CONNECT ? + 'Connect to your Ledger' : + 'Select desired address'; + return ( + +
+ {this.state.stepIndex === LedgerSteps.CONNECT && + this.renderConnectStep() + } + {this.state.stepIndex === LedgerSteps.SELECT_ADDRESS && + this.renderSelectAddressStep() + } +
+
+ ); + } + private renderConnectStep() { + return ( +
+
+ Follow these instructions before proceeding: +
+
    +
  1. + Connect your Ledger Nano S & Open the Ethereum application +
  2. +
  3. + Verify that Browser Support is enabled in Settings +
  4. +
  5. + If no Browser Support is found in settings, verify that you have{' '} + Firmware >1.2 +
  6. +
+
+ + {this.state.didConnectFail && +
+ Failed to connect. Follow the instructions and try again. +
+ } +
+
+ ); + } + private renderSelectAddressStep() { + return ( +
+
+ + + + Address + Balance + + + + {this.renderAddressTableRows()} + +
+
+
+
+ +
+
+ +
+
+
+ ); + } + 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 ( + + +
+ {userAddress} +
+ {userAddress} +
+ +
+ {balanceString} +
+ {balanceString} +
+
+ ); + }); + 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 { + 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 ( + , + ]} + open={props.isOpen} + onRequestClose={props.onToggleDialog.bind(this)} + autoScrollBodyContent={true} + modal={true} + > +
+
+ 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. +
+
+
+ ); +} 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 { + constructor() { + super(); + this.state = { + recipient: '', + shouldShowIncompleteErrs: false, + isAmountValid: false, + }; + } + public render() { + const transferDialogActions = [ + , + , + ]; + return ( + + {this.renderSendDialogBody()} + + ); + } + private renderSendDialogBody() { + return ( +
+
+ +
+ +
+ ); + } + 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 { + constructor(props: TrackTokenConfirmationDialogProps) { + super(props); + this.state = { + isAddingTokenToTracked: false, + }; + } + public render() { + const tokens = this.props.tokens; + return ( + , + , + ]} + open={this.props.isOpen} + onRequestClose={this.props.onToggleDialog.bind(this, false)} + autoScrollBodyContent={true} + > +
+ +
+
+ ); + } + 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 ( + , + ]} + open={props.isOpen} + onRequestClose={props.onToggleDialog.bind(this)} + autoScrollBodyContent={true} + > +
+
+ 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. +
+
+
    +
  • Chrome version 38 or later
  • +
  • Opera version 40 of later
  • +
  • + Firefox with{' '} + + this extension + . +
  • +
+
+
+
+ ); +} -- cgit v1.2.3