aboutsummaryrefslogblamecommitdiffstats
path: root/packages/website/ts/components/wallet/wallet.tsx
blob: 40a8a23eae3f7976b4d08693af43d3733a30b802 (plain) (tree)
1
2
3
4
5
6
7
8
9
                                                                                      
                                                         
                            
 
                                                                                             
                               


                                           
                                                                        
                                                   
                                                       
                                                                          
                                                          
                                                       
                                                 




                                        
                   
                                      
                                             
                                                        
                                                                


                                                                         
                                                                     
                                                                        
                                                 
                                         
        
                 

                   
                 





                        
                                               



                                               

                        
                           




                                   
                                      
                                       

                                 

                              
                                                    
                                     
                           
                              
                                                                    
                               


                       
                                 
                               







                                 
                                 


                                                  
                                            
                          
                             
                                 
                               
                                           
                                         
                             
 
                                                                       


                                  

                                     
                      
                                             
                                     

          













                                                                                                                  
                                      
                
                                                                                 
                                                                                                  
                     

          
                                                   



                                                                                        

                                                              
                                               
                                                             
                

                                                                                    
                                                                                                                     
                   


















                                                                                  

                  
     





                                                                                            
                                                                     


                                                                        
          

                                                                                                               
     
                                                                         
                
                            
                                     







                                                                          

                                              


              
                                                                  
                                                                    
                                                   
                                                     

                                              
                                                                         

                                                              
                                                                                                                        

                  
                                       
                           


















                                                                                                    
                                                                                                                  

                                                                                                            
                                                                              




                                                                          
                                                        
                                                     
              
          
                







                                                                                    

          
                                                   
                                               
                

                                 
                                   




                                                                 
                  

          

                                                  


                                             
                                                                          
                                                              
                               
                                                                                                       

          
                                                                              



                                    
                                        



                                     
                                               
                                                                                                             
                                               
                                                                 
                                         
                                   
                                                            
          
                                               
                                                                                          
                                                 
                                                
                                                                 

                                         
                                                                                         
          
                                     
                                                
          
                                   
                                                                                                  
     
                                                 

                                                                       

                                                                              
                                            


                                                                                           
                                                            
                                                                                


                                        




                                                               
                                                                                            

                                                                        
                                                                                                                       
                                                


                               
                                 
          
                                                          
                                  




                                    
                                  
                                                                                                  

                              
                    



                                                 
                           
                        

                                                               

                                                                                             
                                               














                                                                                                             
                
                                                                                      
                                                                                    


                                
                                                          
                                         
                                                                                  


                                                                               
                  
                                                                                    
                  

          
                                                                                        
                                                                                          
                                                                              






                                                                                                                       
                


                                                                                                                  
                      
                                                             


                  
                                                                                    
                               

                            
                                                  

                                              
                                                        
                                                                                                            


              





                                   

                        



                                                                                    


                              
                                                                                   
         
     





                                   




                                                                           
                



                                                                                   

                          
     
                                                                                     
                                                                                                       
                        
                           

                                          
                                          



                                            
                                                            


                                           
                                                          

                          
                                                                                                    
             
         
                                                   
                                                                                
                                                                                
                
                                                                                                                        

          
                                                                           

                                                                                                                
                                



                                  
                                                                            

                                                                                                                
                                



                                             
                                   
                                                            
     










                                                                                      

 
                                     
import { EtherscanLinkSuffixes, utils as sharedUtils } from '@0xproject/react-shared';
import { BigNumber, errorUtils } from '@0xproject/utils';
import * as _ from 'lodash';

import ActionAccountBalanceWallet from 'material-ui/svg-icons/action/account-balance-wallet';
import * as React from 'react';
import firstBy = require('thenby');

import { Blockchain } from 'ts/blockchain';
import { AccountConnection } from 'ts/components/ui/account_connection';
import { Balance } from 'ts/components/ui/balance';
import { Container } from 'ts/components/ui/container';
import { DropDown, DropdownMouseEvent } from 'ts/components/ui/drop_down';
import { IconButton } from 'ts/components/ui/icon_button';
import { Identicon } from 'ts/components/ui/identicon';
import { Island } from 'ts/components/ui/island';
import {
    CopyAddressSimpleMenuItem,
    DifferentWalletSimpleMenuItem,
    GoToAccountManagementSimpleMenuItem,
    SimpleMenu,
    SimpleMenuItem,
} from 'ts/components/ui/simple_menu';
import { Text } from 'ts/components/ui/text';
import { TokenIcon } from 'ts/components/ui/token_icon';
import { BodyOverlay } from 'ts/components/wallet/body_overlay';
import { NullTokenRow } from 'ts/components/wallet/null_token_row';
import { PlaceHolder } from 'ts/components/wallet/placeholder';
import { StandardIconRow } from 'ts/components/wallet/standard_icon_row';
import { WrapEtherItem } from 'ts/components/wallet/wrap_ether_item';
import { AllowanceToggle } from 'ts/containers/inputs/allowance_toggle';
import { Dispatcher } from 'ts/redux/dispatcher';
import { colors } from 'ts/style/colors';
import {
    AccountState,
    BlockchainErrs,
    ProviderType,
    ScreenWidths,
    Side,
    Token,
    TokenByAddress,
    TokenState,
    TokenStateByAddress,
} from 'ts/types';
import { analytics } from 'ts/utils/analytics';
import { constants } from 'ts/utils/constants';
import { utils } from 'ts/utils/utils';

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;
    screenWidth: ScreenWidths;
    location: Location;
    trackedTokenStateByAddress: TokenStateByAddress;
    onToggleLedgerDialog: () => void;
    onAddToken: () => void;
    onRemoveToken: () => void;
    refetchTokenStateAsync: (tokenAddress: string) => Promise<void>;
    style: React.CSSProperties;
}

interface WalletState {
    wrappedEtherDirection?: Side;
    isHoveringSidebar: boolean;
}

interface AllowanceToggleConfig {
    token: Token;
    tokenState: TokenState;
}

interface AccessoryItemConfig {
    wrappedEtherDirection?: Side;
    allowanceToggleConfig?: AllowanceToggleConfig;
}

const ETHER_ICON_PATH = '/images/ether.png';
const ICON_DIMENSION = 28;
const BODY_ITEM_KEY = 'BODY';
const HEADER_ITEM_KEY = 'HEADER';
const ETHER_ITEM_KEY = 'ETHER';
const NO_ALLOWANCE_TOGGLE_SPACE_WIDTH = 56;
const PLACEHOLDER_COLOR = colors.grey300;
const LOADING_ROWS_COUNT = 6;

export class Wallet extends React.Component<WalletProps, WalletState> {
    public static defaultProps = {
        style: {},
    };
    constructor(props: WalletProps) {
        super(props);
        this.state = {
            wrappedEtherDirection: undefined,
            isHoveringSidebar: false,
        };
    }
    public componentDidUpdate(prevProps: WalletProps): void {
        const currentTrackedTokens = this.props.trackedTokens;
        const differentTrackedTokens = _.difference(currentTrackedTokens, prevProps.trackedTokens);
        const firstDifferentTrackedToken = _.head(differentTrackedTokens);
        // check if there is only one different token, and if that token is a member of the current tracked tokens
        // this means that the token was added, not removed
        if (
            !_.isUndefined(firstDifferentTrackedToken) &&
            _.size(differentTrackedTokens) === 1 &&
            _.includes(currentTrackedTokens, firstDifferentTrackedToken)
        ) {
            document.getElementById(firstDifferentTrackedToken.address).scrollIntoView();
        }
    }
    public render(): React.ReactNode {
        return (
            <Island className="flex flex-column wallet" style={this.props.style}>
                {this._isBlockchainReady() ? this._renderLoadedRows() : this._renderLoadingRows()}
            </Island>
        );
    }
    private _renderLoadingRows(): React.ReactNode {
        return _.concat(this._renderLoadingHeaderRows(), this._renderLoadingBodyRows());
    }
    private _renderLoadingHeaderRows(): React.ReactElement<{}> {
        return this._renderPlainHeaderRow('Loading...');
    }
    private _renderLoadingBodyRows(): React.ReactElement<{}> {
        const bodyStyle = this._getBodyStyle();
        const loadingRowsRange = _.range(LOADING_ROWS_COUNT);
        return (
            <div key={BODY_ITEM_KEY} className="flex flex-column" style={bodyStyle}>
                {_.map(loadingRowsRange, index => {
                    return <NullTokenRow key={index} iconDimension={ICON_DIMENSION} fillColor={PLACEHOLDER_COLOR} />;
                })}
                <Container
                    className="flex items-center"
                    position="absolute"
                    width="100%"
                    height="100%"
                    maxHeight={bodyStyle.maxHeight}
                >
                    <div className="mx-auto">
                        <BodyOverlay
                            dispatcher={this.props.dispatcher}
                            userAddress={this.props.userAddress}
                            injectedProviderName={this.props.injectedProviderName}
                            providerType={this.props.providerType}
                            onToggleLedgerDialog={this.props.onToggleLedgerDialog}
                            blockchain={this.props.blockchain}
                            blockchainIsLoaded={this.props.blockchainIsLoaded}
                        />
                    </div>
                </Container>
            </div>
        );
    }
    private _renderLoadedRows(): React.ReactNode {
        const isAddressAvailable = !_.isEmpty(this.props.userAddress);
        return isAddressAvailable
            ? _.concat(this._renderConnectedHeaderRows(), this._renderBody())
            : _.concat(this._renderDisconnectedHeaderRows(), this._renderLoadingBodyRows());
    }
    private _renderDisconnectedHeaderRows(): React.ReactElement<{}> {
        const isExternallyInjectedProvider = utils.isExternallyInjected(
            this.props.providerType,
            this.props.injectedProviderName,
        );
        const text = isExternallyInjectedProvider ? 'Please unlock MetaMask...' : 'Please connect a wallet...';
        return this._renderPlainHeaderRow(text);
    }
    private _renderPlainHeaderRow(text: string): React.ReactElement<{}> {
        return (
            <StandardIconRow
                key={HEADER_ITEM_KEY}
                icon={<ActionAccountBalanceWallet color={colors.grey} />}
                main={
                    <Text fontSize="16px" fontColor={colors.grey}>
                        {text}
                    </Text>
                    // https://github.com/palantir/tslint-react/issues/140
                    // tslint:disable-next-line:jsx-curly-spacing
                }
                minHeight="60px"
                backgroundColor={colors.white}
            />
        );
    }
    private _renderConnectedHeaderRows(): React.ReactElement<{}> {
        const isMobile = this.props.screenWidth === ScreenWidths.Sm;
        const userAddress = this.props.userAddress;
        const accountState = this._getAccountState();
        const main = (
            <div className="flex flex-column">
                <Text fontSize="16px" lineHeight="19px" fontWeight={500}>
                    {utils.getAddressBeginAndEnd(userAddress)}
                </Text>
                <AccountConnection accountState={accountState} injectedProviderName={this.props.injectedProviderName} />
            </div>
        );
        const onClick = _.noop.bind(_);
        const accessory = (
            <DropDown
                activeNode={
                    // this container gives the menu button more of a hover target for the drop down
                    // it prevents accidentally closing the menu by moving off of the button
                    <Container paddingLeft="100px" paddingRight="15px">
                        <Text
                            className="zmdi zmdi-more-horiz"
                            Tag="i"
                            fontSize="32px"
                            fontFamily="Material-Design-Iconic-Font"
                            fontColor={colors.darkGrey}
                            onClick={onClick}
                            hoverColor={colors.mediumBlue}
                        />
                    </Container>
                }
                popoverContent={
                    <SimpleMenu minWidth="150px">
                        <CopyAddressSimpleMenuItem userAddress={this.props.userAddress} />
                        {!isMobile && <DifferentWalletSimpleMenuItem onClick={this.props.onToggleLedgerDialog} />}
                        <SimpleMenuItem displayText="Add Tokens..." onClick={this.props.onAddToken} />
                        <SimpleMenuItem displayText="Remove Tokens..." onClick={this.props.onRemoveToken} />
                        {!isMobile && <GoToAccountManagementSimpleMenuItem />}
                    </SimpleMenu>
                }
                anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
                targetOrigin={{ horizontal: 'right', vertical: 'top' }}
                zDepth={1}
                activateEvent={DropdownMouseEvent.Click}
                closeEvent={DropdownMouseEvent.Click}
            />
        );
        return (
            <StandardIconRow
                key={HEADER_ITEM_KEY}
                icon={<Identicon address={userAddress} diameter={ICON_DIMENSION} />}
                main={main}
                accessory={accessory}
                minHeight="60px"
                backgroundColor={colors.white}
            />
        );
    }
    private _renderBody(): React.ReactElement<{}> {
        const bodyStyle = this._getBodyStyle();
        return (
            <div
                style={bodyStyle}
                key={BODY_ITEM_KEY}
                onMouseEnter={this._onSidebarHover.bind(this)}
                onMouseLeave={this._onSidebarHoverOff.bind(this)}
            >
                {this._renderEthRows()}
                {this._renderTokenRows()}
            </div>
        );
    }
    private _getBodyStyle(): React.CSSProperties {
        return {
            overflow: 'auto',
            WebkitOverflowScrolling: 'touch',
            position: 'relative',
            overflowY: this.state.isHoveringSidebar ? 'scroll' : 'hidden',
            marginRight: this.state.isHoveringSidebar ? 0 : 4,
            minHeight: '250px',
            maxHeight: !utils.isMobileWidth(this.props.screenWidth) ? 'calc(90vh - 300px)' : undefined,
        };
    }
    private _onSidebarHover(_event: React.FormEvent<HTMLInputElement>): void {
        this.setState({
            isHoveringSidebar: true,
        });
    }
    private _onSidebarHoverOff(): void {
        this.setState({
            isHoveringSidebar: false,
        });
    }
    private _renderEthRows(): React.ReactNode {
        const icon = <img style={{ width: ICON_DIMENSION, height: ICON_DIMENSION }} src={ETHER_ICON_PATH} />;
        const primaryText = this._renderAmount(
            this.props.userEtherBalanceInWei || new BigNumber(0),
            constants.DECIMAL_PLACES_ETH,
            constants.ETHER_SYMBOL,
            _.isUndefined(this.props.userEtherBalanceInWei),
        );
        const etherToken = this._getEthToken();
        const etherTokenState = this.props.trackedTokenStateByAddress[etherToken.address];
        const etherPrice = etherTokenState.price;
        const secondaryText = this._renderValue(
            this.props.userEtherBalanceInWei || new BigNumber(0),
            constants.DECIMAL_PLACES_ETH,
            etherPrice,
            _.isUndefined(this.props.userEtherBalanceInWei) || !etherTokenState.isLoaded,
        );
        const accessoryItemConfig = {
            wrappedEtherDirection: Side.Deposit,
        };
        const key = ETHER_ITEM_KEY;
        return this._renderBalanceRow(key, icon, primaryText, secondaryText, accessoryItemConfig);
    }
    private _renderTokenRows(): React.ReactNode {
        const trackedTokens = this.props.trackedTokens;
        const trackedTokensStartingWithEtherToken = trackedTokens.sort(
            firstBy((t: Token) => t.symbol !== constants.ETHER_TOKEN_SYMBOL)
                .thenBy((t: Token) => t.symbol !== constants.ZRX_TOKEN_SYMBOL)
                .thenBy('trackedTimestamp'),
        );
        return _.map(trackedTokensStartingWithEtherToken, this._renderTokenRow.bind(this));
    }
    private _renderTokenRow(token: Token): React.ReactNode {
        const tokenState = this.props.trackedTokenStateByAddress[token.address];
        if (_.isUndefined(tokenState)) {
            return null;
        }
        const tokenLink = sharedUtils.getEtherScanLinkIfExists(
            token.address,
            this.props.networkId,
            EtherscanLinkSuffixes.Address,
        );
        const icon = <TokenIcon token={token} diameter={ICON_DIMENSION} link={tokenLink} />;
        const isWeth = token.symbol === constants.ETHER_TOKEN_SYMBOL;
        const wrappedEtherDirection = isWeth ? Side.Receive : undefined;
        const primaryText = this._renderAmount(tokenState.balance, token.decimals, token.symbol, !tokenState.isLoaded);
        const secondaryText = this._renderValue(
            tokenState.balance,
            token.decimals,
            tokenState.price,
            !tokenState.isLoaded,
        );
        const accessoryItemConfig: AccessoryItemConfig = {
            wrappedEtherDirection,
            allowanceToggleConfig: {
                token,
                tokenState,
            },
        };
        const key = token.address;
        return this._renderBalanceRow(key, icon, primaryText, secondaryText, accessoryItemConfig);
    }
    private _renderBalanceRow(
        key: string,
        icon: React.ReactNode,
        primaryText: React.ReactNode,
        secondaryText: React.ReactNode,
        accessoryItemConfig: AccessoryItemConfig,
        className?: string,
    ): React.ReactNode {
        const shouldShowWrapEtherItem =
            !_.isUndefined(this.state.wrappedEtherDirection) &&
            this.state.wrappedEtherDirection === accessoryItemConfig.wrappedEtherDirection &&
            !_.isUndefined(this.props.userEtherBalanceInWei);
        const etherToken = this._getEthToken();
        const wrapEtherItem = 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)}
                // tslint:disable:jsx-no-lambda
                refetchEthTokenStateAsync={async () => this.props.refetchTokenStateAsync(etherToken.address)}
            />
        ) : null;
        return (
            <div id={key} key={key} className={`flex flex-column ${className || ''}`}>
                {this.state.wrappedEtherDirection === Side.Receive && wrapEtherItem}
                <StandardIconRow
                    icon={icon}
                    main={
                        <div className="flex flex-column">
                            {primaryText}
                            <Container marginTop="3px">{secondaryText}</Container>
                        </div>
                    }
                    accessory={this._renderAccessoryItems(accessoryItemConfig)}
                />
                {this.state.wrappedEtherDirection === Side.Deposit && wrapEtherItem}
            </div>
        );
    }
    private _renderAccessoryItems(config: AccessoryItemConfig): React.ReactElement<{}> {
        const shouldShowWrappedEtherAction = !_.isUndefined(config.wrappedEtherDirection);
        const shouldShowToggle = !_.isUndefined(config.allowanceToggleConfig);
        // if we don't have a toggle, we still want some space to the right of the "wrap" button so that it aligns with
        // the "unwrap" button in the row below
        const toggle = shouldShowToggle ? (
            this._renderAllowanceToggle(config.allowanceToggleConfig)
        ) : (
            <div style={{ width: NO_ALLOWANCE_TOGGLE_SPACE_WIDTH }} />
        );
        return (
            <div className="flex items-center">
                <div className="flex-auto">
                    {shouldShowWrappedEtherAction && this._renderWrappedEtherButton(config.wrappedEtherDirection)}
                </div>
                <div className="flex-last pl2">{toggle}</div>
            </div>
        );
    }
    private _renderAllowanceToggle(config: AllowanceToggleConfig): React.ReactNode {
        // TODO: Error handling
        return (
            <AllowanceToggle
                blockchain={this.props.blockchain}
                token={config.token}
                tokenState={config.tokenState}
                isDisabled={!config.tokenState.isLoaded}
                refetchTokenStateAsync={async () => this.props.refetchTokenStateAsync(config.token.address)}
            />
        );
    }
    private _renderAmount(
        amount: BigNumber,
        decimals: number,
        symbol: string,
        isLoading: boolean = false,
    ): React.ReactNode {
        if (isLoading) {
            return (
                <PlaceHolder hideChildren={isLoading} fillColor={PLACEHOLDER_COLOR}>
                    <Text fontSize="16px" fontWeight="bold" lineHeight="1em">
                        0.00 XXX
                    </Text>
                </PlaceHolder>
            );
        } else {
            return <Balance amount={amount} decimals={decimals} symbol={symbol} />;
        }
    }
    private _renderValue(
        amount: BigNumber,
        decimals: number,
        price?: BigNumber,
        isLoading: boolean = false,
    ): React.ReactNode {
        const result = !isLoading
            ? _.isUndefined(price)
                ? '--'
                : utils.getUsdValueFormattedAmount(amount, decimals, price)
            : '$0.00';
        return (
            <PlaceHolder hideChildren={isLoading} fillColor={PLACEHOLDER_COLOR}>
                <Text fontSize="14px" fontColor={colors.darkGrey} lineHeight="1em">
                    {result}
                </Text>
            </PlaceHolder>
        );
    }
    private _renderWrappedEtherButton(wrappedEtherDirection: Side): React.ReactNode {
        const isWrappedEtherDirectionOpen = this.state.wrappedEtherDirection === wrappedEtherDirection;
        let buttonLabel;
        let buttonIconName;
        if (isWrappedEtherDirectionOpen) {
            buttonLabel = 'cancel';
            buttonIconName = 'zmdi-close';
        } else {
            switch (wrappedEtherDirection) {
                case Side.Deposit:
                    buttonLabel = 'wrap';
                    buttonIconName = 'zmdi-long-arrow-down';
                    break;
                case Side.Receive:
                    buttonLabel = 'unwrap';
                    buttonIconName = 'zmdi-long-arrow-up';
                    break;
                default:
                    throw errorUtils.spawnSwitchErr('wrappedEtherDirection', wrappedEtherDirection);
            }
        }
        const onClick = isWrappedEtherDirectionOpen
            ? this._closeWrappedEtherActionRow.bind(this, wrappedEtherDirection)
            : this._openWrappedEtherActionRow.bind(this, wrappedEtherDirection);
        return (
            <IconButton iconName={buttonIconName} labelText={buttonLabel} onClick={onClick} color={colors.mediumBlue} />
        );
    }
    private _openWrappedEtherActionRow(wrappedEtherDirection: Side): void {
        const action =
            wrappedEtherDirection === Side.Deposit ? 'Wallet - Wrap ETH Opened' : 'Wallet - Unwrap WETH Opened';
        analytics.track(action);
        this.setState({
            wrappedEtherDirection,
        });
    }
    private _closeWrappedEtherActionRow(wrappedEtherDirection: Side): void {
        const action =
            wrappedEtherDirection === Side.Deposit ? 'Wallet - Wrap ETH Closed' : 'Wallet - Unwrap WETH Closed';
        analytics.track(action);
        this.setState({
            wrappedEtherDirection: undefined,
        });
    }
    private _getEthToken(): Token {
        return utils.getEthToken(this.props.tokenByAddress);
    }
    private _isBlockchainReady(): boolean {
        return this.props.blockchainIsLoaded && !_.isUndefined(this.props.blockchain);
    }
    private _getAccountState(): AccountState {
        return utils.getAccountState(
            this._isBlockchainReady(),
            this.props.providerType,
            this.props.injectedProviderName,
            this.props.userAddress,
        );
    }
}

// tslint:disable:max-file-line-count