aboutsummaryrefslogtreecommitdiffstats
path: root/packages/website/ts/components/wallet
diff options
context:
space:
mode:
authorFabio Berger <me@fabioberger.com>2018-04-02 01:41:28 +0800
committerFabio Berger <me@fabioberger.com>2018-04-02 01:41:28 +0800
commit08b30b0706751eb2a4318f8287dd8a7502276a59 (patch)
tree51e77fd3fc031d1bd7381422c4e8f90263b89e87 /packages/website/ts/components/wallet
parentbf52ce7e725224de23c2a5318b8224cc92e5188a (diff)
parent19454a92dcf29c3c50d3d62cbda3c62b7bafe37f (diff)
downloaddexon-sol-tools-08b30b0706751eb2a4318f8287dd8a7502276a59.tar
dexon-sol-tools-08b30b0706751eb2a4318f8287dd8a7502276a59.tar.gz
dexon-sol-tools-08b30b0706751eb2a4318f8287dd8a7502276a59.tar.bz2
dexon-sol-tools-08b30b0706751eb2a4318f8287dd8a7502276a59.tar.lz
dexon-sol-tools-08b30b0706751eb2a4318f8287dd8a7502276a59.tar.xz
dexon-sol-tools-08b30b0706751eb2a4318f8287dd8a7502276a59.tar.zst
dexon-sol-tools-08b30b0706751eb2a4318f8287dd8a7502276a59.zip
Merge branch 'development' into refactor/publishProcess
* development: Add HACK comment and reorganize use other wallet button Fixed comment(s) for PR #488 OrderStateWatcher fixes for PR #488 Amended comments to clarify changes on PR #488 Monitor different state layers with OrderWatcher Implement wallet locked and uninstalled states Style changes Remove props that are covered by the defaults Implement ETH/WETH conversion and allowance toggle styling
Diffstat (limited to 'packages/website/ts/components/wallet')
-rw-r--r--packages/website/ts/components/wallet/wallet.tsx481
-rw-r--r--packages/website/ts/components/wallet/wallet_disconnected_item.tsx81
-rw-r--r--packages/website/ts/components/wallet/wrap_ether_item.tsx184
3 files changed, 746 insertions, 0 deletions
diff --git a/packages/website/ts/components/wallet/wallet.tsx b/packages/website/ts/components/wallet/wallet.tsx
new file mode 100644
index 000000000..39c95d31c
--- /dev/null
+++ b/packages/website/ts/components/wallet/wallet.tsx
@@ -0,0 +1,481 @@
+import { ZeroEx } from '0x.js';
+import {
+ colors,
+ constants as sharedConstants,
+ EtherscanLinkSuffixes,
+ Styles,
+ utils as sharedUtils,
+} from '@0xproject/react-shared';
+import { BigNumber } from '@0xproject/utils';
+import * as _ from 'lodash';
+import FlatButton from 'material-ui/FlatButton';
+import { List, ListItem } from 'material-ui/List';
+import ActionAccountBalanceWallet from 'material-ui/svg-icons/action/account-balance-wallet';
+import NavigationArrowDownward from 'material-ui/svg-icons/navigation/arrow-downward';
+import NavigationArrowUpward from 'material-ui/svg-icons/navigation/arrow-upward';
+import Close from 'material-ui/svg-icons/navigation/close';
+import * as React from 'react';
+import ReactTooltip = require('react-tooltip');
+import firstBy = require('thenby');
+
+import { Blockchain } from 'ts/blockchain';
+import { AllowanceToggle } from 'ts/components/inputs/allowance_toggle';
+import { Identicon } from 'ts/components/ui/identicon';
+import { TokenIcon } from 'ts/components/ui/token_icon';
+import { WalletDisconnectedItem } from 'ts/components/wallet/wallet_disconnected_item';
+import { WrapEtherItem } from 'ts/components/wallet/wrap_ether_item';
+import { Dispatcher } from 'ts/redux/dispatcher';
+import {
+ BalanceErrs,
+ BlockchainErrs,
+ ProviderType,
+ Side,
+ Token,
+ TokenByAddress,
+ TokenState,
+ TokenStateByAddress,
+} from 'ts/types';
+import { constants } from 'ts/utils/constants';
+import { utils } from 'ts/utils/utils';
+import { styles as walletItemStyles } from 'ts/utils/wallet_item_styles';
+
+export interface WalletProps {
+ userAddress: string;
+ networkId: number;
+ blockchain: Blockchain;
+ blockchainIsLoaded: boolean;
+ blockchainErr: BlockchainErrs;
+ dispatcher: Dispatcher;
+ tokenByAddress: TokenByAddress;
+ trackedTokens: Token[];
+ userEtherBalanceInWei: BigNumber;
+ lastForceTokenStateRefetch: number;
+ injectedProviderName: string;
+ providerType: ProviderType;
+ onToggleLedgerDialog: () => void;
+}
+
+interface WalletState {
+ trackedTokenStateByAddress: TokenStateByAddress;
+ wrappedEtherDirection?: Side;
+}
+
+interface AllowanceToggleConfig {
+ token: Token;
+ tokenState: TokenState;
+}
+
+interface AccessoryItemConfig {
+ wrappedEtherDirection?: Side;
+ allowanceToggleConfig?: AllowanceToggleConfig;
+}
+
+const styles: Styles = {
+ wallet: {
+ width: 346,
+ backgroundColor: colors.white,
+ borderBottomRightRadius: 10,
+ borderBottomLeftRadius: 10,
+ borderTopRightRadius: 10,
+ borderTopLeftRadius: 10,
+ boxShadow: `0px 4px 6px ${colors.walletBoxShadow}`,
+ overflow: 'hidden',
+ },
+ list: {
+ padding: 0,
+ },
+ tokenItemInnerDiv: {
+ paddingLeft: 60,
+ },
+ headerItemInnerDiv: {
+ paddingLeft: 65,
+ },
+ footerItemInnerDiv: {
+ paddingLeft: 24,
+ },
+ borderedItem: {
+ borderBottomColor: colors.walletBorder,
+ borderBottomStyle: 'solid',
+ borderWidth: 1,
+ },
+ tokenItem: {
+ backgroundColor: colors.walletDefaultItemBackground,
+ },
+ wrappedEtherOpenButtonLabel: {
+ fontSize: 10,
+ },
+ amountLabel: {
+ fontWeight: 'bold',
+ color: colors.black,
+ },
+ paddedItem: {
+ paddingTop: 8,
+ paddingBottom: 8,
+ },
+ accessoryItemsContainer: { width: 150, right: 8 },
+};
+
+const ETHER_ICON_PATH = '/images/ether.png';
+const ETHER_TOKEN_SYMBOL = 'WETH';
+const ZRX_TOKEN_SYMBOL = 'ZRX';
+const ETHER_SYMBOL = 'ETH';
+const ICON_DIMENSION = 24;
+const TOKEN_AMOUNT_DISPLAY_PRECISION = 3;
+
+export class Wallet extends React.Component<WalletProps, WalletState> {
+ private _isUnmounted: boolean;
+ constructor(props: WalletProps) {
+ super(props);
+ this._isUnmounted = false;
+ const initialTrackedTokenStateByAddress = this._getInitialTrackedTokenStateByAddress(props.trackedTokens);
+ this.state = {
+ trackedTokenStateByAddress: initialTrackedTokenStateByAddress,
+ wrappedEtherDirection: undefined,
+ };
+ }
+ 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: WalletProps) {
+ 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;
+ _.each(newTokenAddresses, (tokenAddress: string) => {
+ trackedTokenStateByAddress[tokenAddress] = {
+ balance: new BigNumber(0),
+ allowance: new BigNumber(0),
+ isLoaded: false,
+ };
+ });
+ this.setState({
+ trackedTokenStateByAddress,
+ });
+ // Fetch the actual balance/allowance.
+ // tslint:disable-next-line:no-floating-promises
+ this._fetchBalancesAndAllowancesAsync(newTokenAddresses);
+ }
+ }
+ public render() {
+ const isReadyToRender = this.props.blockchainIsLoaded && this.props.blockchainErr === BlockchainErrs.NoError;
+ return <div style={styles.wallet}>{isReadyToRender && this._renderRows()}</div>;
+ }
+ private _renderRows() {
+ const isAddressAvailable = !_.isEmpty(this.props.userAddress);
+ return (
+ <List style={styles.list}>
+ {isAddressAvailable
+ ? _.concat(
+ this._renderConnectedHeaderRows(),
+ this._renderEthRows(),
+ this._renderTokenRows(),
+ this._renderFooterRows(),
+ )
+ : _.concat(this._renderDisconnectedHeaderRows(), this._renderDisconnectedRows())}
+ </List>
+ );
+ }
+ private _renderDisconnectedHeaderRows() {
+ const userAddress = this.props.userAddress;
+ const primaryText = 'wallet';
+ return (
+ <ListItem
+ primaryText={primaryText.toUpperCase()}
+ leftIcon={<ActionAccountBalanceWallet color={colors.mediumBlue} />}
+ style={styles.paddedItem}
+ innerDivStyle={styles.headerItemInnerDiv}
+ />
+ );
+ }
+ private _renderDisconnectedRows() {
+ return (
+ <WalletDisconnectedItem
+ providerType={this.props.providerType}
+ injectedProviderName={this.props.injectedProviderName}
+ onToggleLedgerDialog={this.props.onToggleLedgerDialog}
+ />
+ );
+ }
+ private _renderConnectedHeaderRows() {
+ const userAddress = this.props.userAddress;
+ const primaryText = utils.getAddressBeginAndEnd(userAddress);
+ return (
+ <ListItem
+ primaryText={primaryText}
+ leftIcon={<Identicon address={userAddress} diameter={ICON_DIMENSION} />}
+ style={{ ...styles.paddedItem, ...styles.borderedItem }}
+ innerDivStyle={styles.headerItemInnerDiv}
+ />
+ );
+ }
+ private _renderFooterRows() {
+ const primaryText = '+ other tokens';
+ return <ListItem primaryText={primaryText} innerDivStyle={styles.footerItemInnerDiv} />;
+ }
+ private _renderEthRows() {
+ const primaryText = this._renderAmount(
+ this.props.userEtherBalanceInWei,
+ constants.DECIMAL_PLACES_ETH,
+ ETHER_SYMBOL,
+ );
+ const accessoryItemConfig = {
+ wrappedEtherDirection: Side.Deposit,
+ };
+ const isInWrappedEtherState =
+ !_.isUndefined(this.state.wrappedEtherDirection) &&
+ this.state.wrappedEtherDirection === accessoryItemConfig.wrappedEtherDirection;
+ const style = isInWrappedEtherState
+ ? { ...walletItemStyles.focusedItem, ...styles.paddedItem }
+ : { ...styles.tokenItem, ...styles.borderedItem, ...styles.paddedItem };
+ const etherToken = this._getEthToken();
+ return (
+ <div>
+ <ListItem
+ primaryText={primaryText}
+ leftIcon={<img style={{ width: ICON_DIMENSION, height: ICON_DIMENSION }} src={ETHER_ICON_PATH} />}
+ rightAvatar={this._renderAccessoryItems(accessoryItemConfig)}
+ disableTouchRipple={true}
+ style={style}
+ innerDivStyle={styles.tokenItemInnerDiv}
+ />
+ {isInWrappedEtherState && (
+ <WrapEtherItem
+ userAddress={this.props.userAddress}
+ networkId={this.props.networkId}
+ blockchain={this.props.blockchain}
+ dispatcher={this.props.dispatcher}
+ userEtherBalanceInWei={this.props.userEtherBalanceInWei}
+ direction={accessoryItemConfig.wrappedEtherDirection}
+ etherToken={etherToken}
+ lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
+ onConversionSuccessful={this._closeWrappedEtherActionRow.bind(this)}
+ refetchEthTokenStateAsync={this._refetchTokenStateAsync.bind(this, etherToken.address)}
+ />
+ )}
+ </div>
+ );
+ }
+ private _renderTokenRows() {
+ 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)
+ .thenBy('address'),
+ );
+ return _.map(trackedTokensStartingWithEtherToken, this._renderTokenRow.bind(this));
+ }
+ private _renderTokenRow(token: Token) {
+ const tokenState = this.state.trackedTokenStateByAddress[token.address];
+ const tokenLink = sharedUtils.getEtherScanLinkIfExists(
+ token.address,
+ this.props.networkId,
+ EtherscanLinkSuffixes.Address,
+ );
+ const amount = this._renderAmount(tokenState.balance, token.decimals, token.symbol);
+ const wrappedEtherDirection = token.symbol === ETHER_TOKEN_SYMBOL ? Side.Receive : undefined;
+ const accessoryItemConfig: AccessoryItemConfig = {
+ wrappedEtherDirection,
+ allowanceToggleConfig: {
+ token,
+ tokenState,
+ },
+ };
+ const shouldShowWrapEtherItem =
+ !_.isUndefined(this.state.wrappedEtherDirection) &&
+ this.state.wrappedEtherDirection === accessoryItemConfig.wrappedEtherDirection;
+ const style = shouldShowWrapEtherItem
+ ? { ...walletItemStyles.focusedItem, ...styles.paddedItem }
+ : { ...styles.tokenItem, ...styles.borderedItem, ...styles.paddedItem };
+ const etherToken = this._getEthToken();
+ return (
+ <div>
+ <ListItem
+ primaryText={amount}
+ leftIcon={this._renderTokenIcon(token, tokenLink)}
+ rightAvatar={this._renderAccessoryItems(accessoryItemConfig)}
+ disableTouchRipple={true}
+ style={style}
+ innerDivStyle={styles.tokenItemInnerDiv}
+ />
+ {shouldShowWrapEtherItem && (
+ <WrapEtherItem
+ userAddress={this.props.userAddress}
+ networkId={this.props.networkId}
+ blockchain={this.props.blockchain}
+ dispatcher={this.props.dispatcher}
+ userEtherBalanceInWei={this.props.userEtherBalanceInWei}
+ direction={accessoryItemConfig.wrappedEtherDirection}
+ etherToken={etherToken}
+ lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
+ onConversionSuccessful={this._closeWrappedEtherActionRow.bind(this)}
+ refetchEthTokenStateAsync={this._refetchTokenStateAsync.bind(this, etherToken.address)}
+ />
+ )}
+ </div>
+ );
+ }
+ private _renderAccessoryItems(config: AccessoryItemConfig) {
+ const shouldShowWrappedEtherAction = !_.isUndefined(config.wrappedEtherDirection);
+ const shouldShowToggle = !_.isUndefined(config.allowanceToggleConfig);
+ return (
+ <div style={styles.accessoryItemsContainer}>
+ <div className="flex">
+ <div className="flex-auto">
+ {shouldShowWrappedEtherAction && this._renderWrappedEtherButton(config.wrappedEtherDirection)}
+ </div>
+ <div className="flex-last py1">
+ {shouldShowToggle && this._renderAllowanceToggle(config.allowanceToggleConfig)}
+ </div>
+ </div>
+ </div>
+ );
+ }
+ private _renderAllowanceToggle(config: AllowanceToggleConfig) {
+ return (
+ <AllowanceToggle
+ networkId={this.props.networkId}
+ blockchain={this.props.blockchain}
+ dispatcher={this.props.dispatcher}
+ token={config.token}
+ tokenState={config.tokenState}
+ onErrorOccurred={_.noop} // TODO: Error handling
+ userAddress={this.props.userAddress}
+ isDisabled={!config.tokenState.isLoaded}
+ refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this, config.token.address)}
+ />
+ );
+ }
+ private _renderAmount(amount: BigNumber, decimals: number, symbol: string) {
+ const unitAmount = ZeroEx.toUnitAmount(amount, decimals);
+ const formattedAmount = unitAmount.toPrecision(TOKEN_AMOUNT_DISPLAY_PRECISION);
+ const result = `${formattedAmount} ${symbol}`;
+ return <div style={styles.amountLabel}>{result}</div>;
+ }
+ private _renderTokenIcon(token: Token, tokenLink?: string) {
+ const tooltipId = `tooltip-${token.address}`;
+ const tokenIcon = <TokenIcon token={token} diameter={ICON_DIMENSION} />;
+ if (_.isUndefined(tokenLink)) {
+ return tokenIcon;
+ } else {
+ return (
+ <a href={tokenLink} target="_blank" style={{ textDecoration: 'none' }}>
+ {tokenIcon}
+ </a>
+ );
+ }
+ }
+ private _renderWrappedEtherButton(wrappedEtherDirection: Side) {
+ const isWrappedEtherDirectionOpen = this.state.wrappedEtherDirection === wrappedEtherDirection;
+ let buttonLabel;
+ let buttonIcon;
+ if (isWrappedEtherDirectionOpen) {
+ buttonLabel = 'cancel';
+ buttonIcon = <Close />;
+ } else {
+ switch (wrappedEtherDirection) {
+ case Side.Deposit:
+ buttonLabel = 'wrap';
+ buttonIcon = <NavigationArrowDownward />;
+ break;
+ case Side.Receive:
+ buttonLabel = 'unwrap';
+ buttonIcon = <NavigationArrowUpward />;
+ break;
+ default:
+ throw utils.spawnSwitchErr('wrappedEtherDirection', wrappedEtherDirection);
+ }
+ }
+ const onClick = isWrappedEtherDirectionOpen
+ ? this._closeWrappedEtherActionRow.bind(this)
+ : this._openWrappedEtherActionRow.bind(this, wrappedEtherDirection);
+ return (
+ <FlatButton
+ label={buttonLabel}
+ labelPosition="after"
+ primary={true}
+ icon={buttonIcon}
+ labelStyle={styles.wrappedEtherOpenButtonLabel}
+ onClick={onClick}
+ />
+ );
+ }
+ 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 _fetchBalancesAndAllowancesAsync(tokenAddresses: string[]) {
+ const trackedTokenStateByAddress = this.state.trackedTokenStateByAddress;
+ const userAddressIfExists = _.isEmpty(this.props.userAddress) ? undefined : this.props.userAddress;
+ for (const tokenAddress of tokenAddresses) {
+ const [balance, allowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync(
+ userAddressIfExists,
+ tokenAddress,
+ );
+ trackedTokenStateByAddress[tokenAddress] = {
+ balance,
+ allowance,
+ isLoaded: true,
+ };
+ }
+ if (!this._isUnmounted) {
+ this.setState({
+ trackedTokenStateByAddress,
+ });
+ }
+ }
+ private async _refetchTokenStateAsync(tokenAddress: string) {
+ const userAddressIfExists = _.isEmpty(this.props.userAddress) ? undefined : this.props.userAddress;
+ const [balance, allowance] = await this.props.blockchain.getTokenBalanceAndAllowanceAsync(
+ userAddressIfExists,
+ tokenAddress,
+ );
+ this.setState({
+ trackedTokenStateByAddress: {
+ ...this.state.trackedTokenStateByAddress,
+ [tokenAddress]: {
+ balance,
+ allowance,
+ isLoaded: true,
+ },
+ },
+ });
+ }
+ private _openWrappedEtherActionRow(wrappedEtherDirection: Side) {
+ this.setState({
+ wrappedEtherDirection,
+ });
+ }
+ private _closeWrappedEtherActionRow() {
+ this.setState({
+ wrappedEtherDirection: undefined,
+ });
+ }
+ private _getEthToken() {
+ const tokens = _.values(this.props.tokenByAddress);
+ const etherToken = _.find(tokens, { symbol: ETHER_TOKEN_SYMBOL });
+ return etherToken;
+ }
+}
diff --git a/packages/website/ts/components/wallet/wallet_disconnected_item.tsx b/packages/website/ts/components/wallet/wallet_disconnected_item.tsx
new file mode 100644
index 000000000..89e32f7be
--- /dev/null
+++ b/packages/website/ts/components/wallet/wallet_disconnected_item.tsx
@@ -0,0 +1,81 @@
+import { colors, Styles } from '@0xproject/react-shared';
+import FlatButton from 'material-ui/FlatButton';
+import ActionAccountBalanceWallet from 'material-ui/svg-icons/action/account-balance-wallet';
+import * as React from 'react';
+
+import { ProviderType } from 'ts/types';
+import { constants } from 'ts/utils/constants';
+
+export interface WalletDisconnectedItemProps {
+ providerType: ProviderType;
+ injectedProviderName: string;
+ onToggleLedgerDialog: () => void;
+}
+
+const styles: Styles = {
+ button: {
+ border: colors.walletBorder,
+ borderStyle: 'solid',
+ borderWidth: 1,
+ height: 80,
+ },
+ hrefAdjustment: {
+ paddingTop: 20, // HACK: For some reason when we set the href prop of a FlatButton material-ui reduces the top padding
+ },
+ otherWalletText: {
+ fontSize: 14,
+ color: colors.grey500,
+ textDecoration: 'underline',
+ },
+};
+
+const ITEM_HEIGHT = 292;
+const METAMASK_ICON_WIDTH = 35;
+const LEDGER_ICON_WIDTH = 30;
+const BUTTON_BOTTOM_PADDING = 80;
+
+export const WalletDisconnectedItem: React.StatelessComponent<WalletDisconnectedItemProps> = (
+ props: WalletDisconnectedItemProps,
+) => {
+ const isExternallyInjectedProvider =
+ props.providerType === ProviderType.Injected && props.injectedProviderName !== '0x Public';
+ return (
+ <div className="flex flex-center">
+ <div className="mx-auto">
+ <div className="table" style={{ height: ITEM_HEIGHT }}>
+ <div className="table-cell align-middle">
+ <ProviderButton isExternallyInjectedProvider={isExternallyInjectedProvider} />
+ <div className="flex flex-center py2" style={{ paddingBottom: BUTTON_BOTTOM_PADDING }}>
+ <div className="mx-auto">
+ <div onClick={props.onToggleLedgerDialog} style={{ cursor: 'pointer' }}>
+ <img src="/images/ledger_icon.png" style={{ width: LEDGER_ICON_WIDTH }} />
+ <span className="px1" style={styles.otherWalletText}>
+ user other wallet
+ </span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+};
+
+interface ProviderButtonProps {
+ isExternallyInjectedProvider: boolean;
+}
+
+const ProviderButton: React.StatelessComponent<ProviderButtonProps> = (props: ProviderButtonProps) => (
+ <FlatButton
+ label={props.isExternallyInjectedProvider ? 'Please unlock account' : 'Get Metamask Wallet Extension'}
+ labelStyle={{ color: colors.black }}
+ labelPosition="after"
+ primary={true}
+ icon={<img src="/images/metamask_icon.png" width={METAMASK_ICON_WIDTH.toString()} />}
+ style={props.isExternallyInjectedProvider ? styles.button : { ...styles.button, ...styles.hrefAdjustment }}
+ href={props.isExternallyInjectedProvider ? undefined : constants.URL_METAMASK_CHROME_STORE}
+ target={props.isExternallyInjectedProvider ? undefined : '_blank'}
+ disabled={props.isExternallyInjectedProvider}
+ />
+);
diff --git a/packages/website/ts/components/wallet/wrap_ether_item.tsx b/packages/website/ts/components/wallet/wrap_ether_item.tsx
new file mode 100644
index 000000000..3a876721a
--- /dev/null
+++ b/packages/website/ts/components/wallet/wrap_ether_item.tsx
@@ -0,0 +1,184 @@
+import { ZeroEx } from '0x.js';
+import { colors, Styles } from '@0xproject/react-shared';
+import { BigNumber, logUtils } from '@0xproject/utils';
+import * as _ from 'lodash';
+import FlatButton from 'material-ui/FlatButton';
+import { ListItem } from 'material-ui/List';
+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 { Dispatcher } from 'ts/redux/dispatcher';
+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';
+import { styles as walletItemStyles } from 'ts/utils/wallet_item_styles';
+
+export interface WrapEtherItemProps {
+ userAddress: string;
+ networkId: number;
+ blockchain: Blockchain;
+ dispatcher: Dispatcher;
+ userEtherBalanceInWei: BigNumber;
+ direction: Side;
+ etherToken: Token;
+ lastForceTokenStateRefetch: number;
+ onConversionSuccessful?: () => void;
+ refetchEthTokenStateAsync: () => Promise<void>;
+}
+
+interface WrapEtherItemState {
+ currentInputAmount?: BigNumber;
+ currentInputHasErrors: boolean;
+ isEthConversionHappening: boolean;
+}
+
+const styles: Styles = {
+ topLabel: { color: colors.black, fontSize: 11 },
+ inputContainer: {
+ backgroundColor: colors.white,
+ borderBottomRightRadius: 3,
+ borderBottomLeftRadius: 3,
+ borderTopRightRadius: 3,
+ borderTopLeftRadius: 3,
+ padding: 4,
+ width: 125,
+ },
+ ethAmountInput: { height: 32 },
+ innerDiv: { paddingLeft: 60, paddingTop: 0 },
+ wrapEtherConfirmationButtonContainer: { width: 128, top: 16 },
+ wrapEtherConfirmationButtonLabel: {
+ fontSize: 10,
+ color: colors.white,
+ },
+};
+
+export class WrapEtherItem extends React.Component<WrapEtherItemProps, WrapEtherItemState> {
+ constructor(props: WrapEtherItemProps) {
+ super(props);
+ this.state = {
+ currentInputAmount: undefined,
+ currentInputHasErrors: false,
+ isEthConversionHappening: false,
+ };
+ }
+ public render() {
+ const etherBalanceInEth = ZeroEx.toUnitAmount(this.props.userEtherBalanceInWei, constants.DECIMAL_PLACES_ETH);
+ const isWrappingEth = this.props.direction === Side.Deposit;
+ const topLabelText = isWrappingEth ? 'Convert ETH into WETH 1:1' : 'Convert WETH into ETH 1:1';
+ return (
+ <ListItem
+ primaryText={
+ <div>
+ <div style={styles.topLabel}>{topLabelText}</div>
+ <div style={styles.inputContainer}>
+ {isWrappingEth ? (
+ <EthAmountInput
+ balance={etherBalanceInEth}
+ amount={this.state.currentInputAmount}
+ hintText="0.00"
+ onChange={this._onValueChange.bind(this)}
+ shouldCheckBalance={true}
+ shouldShowIncompleteErrs={false}
+ shouldShowErrs={false}
+ shouldShowUnderline={false}
+ style={styles.ethAmountInput}
+ />
+ ) : (
+ <TokenAmountInput
+ lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
+ blockchain={this.props.blockchain}
+ userAddress={this.props.userAddress}
+ networkId={this.props.networkId}
+ token={this.props.etherToken}
+ shouldShowIncompleteErrs={false}
+ shouldCheckBalance={true}
+ shouldCheckAllowance={false}
+ onChange={this._onValueChange.bind(this)}
+ amount={this.state.currentInputAmount}
+ hintText="0.00"
+ shouldShowErrs={false} // TODO: error handling
+ shouldShowUnderline={false}
+ style={styles.ethAmountInput}
+ />
+ )}
+ </div>
+ </div>
+ }
+ secondaryTextLines={2}
+ disableTouchRipple={true}
+ style={walletItemStyles.focusedItem}
+ innerDivStyle={styles.innerDiv}
+ leftIcon={this.state.isEthConversionHappening && this._renderIsEthConversionHappeningSpinner()}
+ rightAvatar={this._renderWrapEtherConfirmationButton()}
+ />
+ );
+ }
+ private _onValueChange(isValid: boolean, amount?: BigNumber) {
+ this.setState({
+ currentInputAmount: amount,
+ currentInputHasErrors: !isValid,
+ });
+ }
+ private _renderIsEthConversionHappeningSpinner() {
+ return (
+ <div className="pl1" style={{ paddingTop: 10 }}>
+ <i className="zmdi zmdi-spinner zmdi-hc-spin" />
+ </div>
+ );
+ }
+ private _renderWrapEtherConfirmationButton() {
+ const isWrappingEth = this.props.direction === Side.Deposit;
+ const labelText = isWrappingEth ? 'wrap' : 'unwrap';
+ return (
+ <div style={styles.wrapEtherConfirmationButtonContainer}>
+ <FlatButton
+ backgroundColor={colors.wrapEtherConfirmationButton}
+ label={labelText}
+ labelStyle={styles.wrapEtherConfirmationButtonLabel}
+ onClick={this._wrapEtherConfirmationAction.bind(this)}
+ disabled={this.state.isEthConversionHappening}
+ />
+ </div>
+ );
+ }
+ private async _wrapEtherConfirmationAction() {
+ this.setState({
+ isEthConversionHappening: true,
+ });
+ try {
+ const etherToken = this.props.etherToken;
+ const amountToConvert = this.state.currentInputAmount;
+ if (this.props.direction === Side.Deposit) {
+ await this.props.blockchain.convertEthToWrappedEthTokensAsync(etherToken.address, amountToConvert);
+ const ethAmount = ZeroEx.toUnitAmount(amountToConvert, constants.DECIMAL_PLACES_ETH);
+ this.props.dispatcher.showFlashMessage(`Successfully wrapped ${ethAmount.toString()} ETH to WETH`);
+ } else {
+ await this.props.blockchain.convertWrappedEthTokensToEthAsync(etherToken.address, amountToConvert);
+ const tokenAmount = ZeroEx.toUnitAmount(amountToConvert, etherToken.decimals);
+ this.props.dispatcher.showFlashMessage(`Successfully unwrapped ${tokenAmount.toString()} WETH to ETH`);
+ }
+ await this.props.refetchEthTokenStateAsync();
+ this.props.onConversionSuccessful();
+ } catch (err) {
+ const errMsg = `${err}`;
+ if (_.includes(errMsg, BlockchainCallErrs.UserHasNoAssociatedAddresses)) {
+ this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true);
+ } else if (!utils.didUserDenyWeb3Request(errMsg)) {
+ logUtils.log(`Unexpected error encountered: ${err}`);
+ logUtils.log(err.stack);
+ const errorMsg =
+ this.props.direction === Side.Deposit
+ ? 'Failed to wrap your ETH. Please try again.'
+ : 'Failed to unwrap your WETH. Please try again.';
+ this.props.dispatcher.showFlashMessage(errorMsg);
+ await errorReporter.reportAsync(err);
+ }
+ }
+ this.setState({
+ isEthConversionHappening: false,
+ });
+ }
+}