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




                                                                                
                                                                                      
                                                                                                      
                                                       
                                                   



                                                           
                                                                       




                                                                    

                                                     
        
                   


                 
          
                   


                        


                                             



























                                             


                                    

 



                                                                             





                                                                                                                   



                                                                                                          






                                                                                                                      



                                                    


                                                                                        







                                                                           











                                                                                                       
                                                            





                                                                                    
                                                            



                                                                                        
                                                                






                                                                             
                                                            

                                                                                        







                                                                                                                 



















































                                                                                                                    



                                                                                             






























                                                                                                    



                                                                             
                                           
                                                                  










                                                                                   












                                                                    























































                                                                                       






                                                                                     






































                                                                                    
import BigNumber from 'bignumber.js';
import * as _ from 'lodash';
import Paper from 'material-ui/Paper';
import {colors} from 'material-ui/styles';
import * as React from 'react';
import * as DocumentTitle from 'react-document-title';
import {Route, Switch} from 'react-router-dom';
import {Blockchain} from 'ts/blockchain';
import {BlockchainErrDialog} from 'ts/components/dialogs/blockchain_err_dialog';
import {PortalDisclaimerDialog} from 'ts/components/dialogs/portal_disclaimer_dialog';
import {WrappedEthSectionNoticeDialog} from 'ts/components/dialogs/wrapped_eth_section_notice_dialog';
import {EthWrappers} from 'ts/components/eth_wrappers';
import {FillOrder} from 'ts/components/fill_order';
import {Footer} from 'ts/components/footer';
import {PortalMenu} from 'ts/components/portal_menu';
import {TokenBalances} from 'ts/components/token_balances';
import {TopBar} from 'ts/components/top_bar';
import {TradeHistory} from 'ts/components/trade_history/trade_history';
import {FlashMessage} from 'ts/components/ui/flash_message';
import {Loading} from 'ts/components/ui/loading';
import {GenerateOrderForm} from 'ts/containers/generate_order_form';
import {localStorage} from 'ts/local_storage/local_storage';
import {Dispatcher} from 'ts/redux/dispatcher';
import {orderSchema} from 'ts/schemas/order_schema';
import {SchemaValidator} from 'ts/schemas/validator';
import {
    BlockchainErrs,
    HashData,
    Order,
    ScreenWidths,
    Token,
    TokenByAddress,
    TokenStateByAddress,
    WebsitePaths,
} from 'ts/types';
import {configs} from 'ts/utils/configs';
import {constants} from 'ts/utils/constants';
import {utils} from 'ts/utils/utils';

const THROTTLE_TIMEOUT = 100;

export interface PortalPassedProps {}

export interface PortalAllProps {
    blockchainErr: BlockchainErrs;
    blockchainIsLoaded: boolean;
    dispatcher: Dispatcher;
    hashData: HashData;
    networkId: number;
    nodeVersion: string;
    orderFillAmount: BigNumber;
    screenWidth: ScreenWidths;
    tokenByAddress: TokenByAddress;
    tokenStateByAddress: TokenStateByAddress;
    userEtherBalance: BigNumber;
    userAddress: string;
    shouldBlockchainErrDialogBeOpen: boolean;
    userSuppliedOrderCache: Order;
    location: Location;
    flashMessage?: string|React.ReactNode;
}

interface PortalAllState {
    prevNetworkId: number;
    prevNodeVersion: string;
    prevUserAddress: string;
    prevPathname: string;
    isDisclaimerDialogOpen: boolean;
    isWethNoticeDialogOpen: boolean;
}

export class Portal extends React.Component<PortalAllProps, PortalAllState> {
    private blockchain: Blockchain;
    private sharedOrderIfExists: Order;
    private throttledScreenWidthUpdate: () => void;
    public static hasAlreadyDismissedWethNotice() {
        const didDismissWethNotice = localStorage.getItemIfExists(constants.DISMISS_WETH_NOTICE_LOCAL_STORAGE_KEY);
        const hasAlreadyDismissedWethNotice = !_.isUndefined(didDismissWethNotice) &&
                                              !_.isEmpty(didDismissWethNotice);
        return hasAlreadyDismissedWethNotice;
    }
    constructor(props: PortalAllProps) {
        super(props);
        this.sharedOrderIfExists = this.getSharedOrderIfExists();
        this.throttledScreenWidthUpdate = _.throttle(this.updateScreenWidth.bind(this), THROTTLE_TIMEOUT);

        const isViewingBalances = _.includes(props.location.pathname, `${WebsitePaths.Portal}/balances`);
        const hasAlreadyDismissedWethNotice = Portal.hasAlreadyDismissedWethNotice();

        const didAcceptPortalDisclaimer = localStorage.getItemIfExists(constants.ACCEPT_DISCLAIMER_LOCAL_STORAGE_KEY);
        const hasAcceptedDisclaimer = !_.isUndefined(didAcceptPortalDisclaimer) &&
                                      !_.isEmpty(didAcceptPortalDisclaimer);
        this.state = {
            prevNetworkId: this.props.networkId,
            prevNodeVersion: this.props.nodeVersion,
            prevUserAddress: this.props.userAddress,
            prevPathname: this.props.location.pathname,
            isDisclaimerDialogOpen: !hasAcceptedDisclaimer,
            isWethNoticeDialogOpen: !hasAlreadyDismissedWethNotice && isViewingBalances,
        };
    }
    public componentDidMount() {
        window.addEventListener('resize', this.throttledScreenWidthUpdate);
        window.scrollTo(0, 0);
    }
    public componentWillMount() {
        this.blockchain = new Blockchain(this.props.dispatcher);
    }
    public componentWillUnmount() {
        this.blockchain.destroy();
        window.removeEventListener('resize', this.throttledScreenWidthUpdate);
        // We re-set the entire redux state when the portal is unmounted so that when it is re-rendered
        // the initialization process always occurs from the same base state. This helps avoid
        // initialization inconsistencies (i.e While the portal was unrendered, the user might have
        // become disconnected from their backing Ethereum node, changes user accounts, etc...)
        this.props.dispatcher.resetState();
    }
    public componentWillReceiveProps(nextProps: PortalAllProps) {
        if (nextProps.networkId !== this.state.prevNetworkId) {
            // tslint:disable-next-line:no-floating-promises
            this.blockchain.networkIdUpdatedFireAndForgetAsync(nextProps.networkId);
            this.setState({
                prevNetworkId: nextProps.networkId,
            });
        }
        if (nextProps.userAddress !== this.state.prevUserAddress) {
            // tslint:disable-next-line:no-floating-promises
            this.blockchain.userAddressUpdatedFireAndForgetAsync(nextProps.userAddress);
            if (!_.isEmpty(nextProps.userAddress) &&
                nextProps.blockchainIsLoaded) {
                const tokens = _.values(nextProps.tokenByAddress);
                // tslint:disable-next-line:no-floating-promises
                this.updateBalanceAndAllowanceWithLoadingScreenAsync(tokens);
            }
            this.setState({
                prevUserAddress: nextProps.userAddress,
            });
        }
        if (nextProps.nodeVersion !== this.state.prevNodeVersion) {
            // tslint:disable-next-line:no-floating-promises
            this.blockchain.nodeVersionUpdatedFireAndForgetAsync(nextProps.nodeVersion);
        }
        if (nextProps.location.pathname !== this.state.prevPathname) {
            const isViewingBalances = _.includes(nextProps.location.pathname, `${WebsitePaths.Portal}/balances`);
            const hasAlreadyDismissedWethNotice = Portal.hasAlreadyDismissedWethNotice();
            this.setState({
                prevPathname: nextProps.location.pathname,
                isWethNoticeDialogOpen: !hasAlreadyDismissedWethNotice && isViewingBalances,
            });
        }
    }
    public render() {
        const updateShouldBlockchainErrDialogBeOpen = this.props.dispatcher
                .updateShouldBlockchainErrDialogBeOpen.bind(this.props.dispatcher);
        const portalStyle: React.CSSProperties = {
            minHeight: '100vh',
            display: 'flex',
            flexDirection: 'column',
            justifyContent: 'space-between',
        };
        return (
            <div style={portalStyle}>
                <DocumentTitle title="0x Portal DApp"/>
                <TopBar
                    userAddress={this.props.userAddress}
                    blockchainIsLoaded={this.props.blockchainIsLoaded}
                    location={this.props.location}
                />
                <div id="portal" className="mx-auto max-width-4 pt4" style={{width: '100%'}}>
                    <Paper className="mb3 mt2">
                        {!configs.isMainnetEnabled && this.props.networkId === constants.MAINNET_NETWORK_ID  ?
                            <div className="p3 center">
                                <div className="h2 py2">Mainnet unavailable</div>
                                <div className="mx-auto pb2 pt2">
                                    <img
                                        src="/images/zrx_token.png"
                                        style={{width: 150}}
                                    />
                                </div>
                                <div>
                                    0x portal is currently unavailable on the Ethereum mainnet.
                                    <div>
                                        To try it out, switch to the Kovan test network
                                        (networkId: 42).
                                    </div>
                                    <div className="py2">
                                        Check back soon!
                                    </div>
                                </div>
                            </div> :
                            <div className="mx-auto flex">
                                <div
                                    className="col col-2 pr2 pt1 sm-hide xs-hide"
                                    style={{overflow: 'hidden', backgroundColor: 'rgb(39, 39, 39)', color: 'white'}}
                                >
                                    <PortalMenu menuItemStyle={{color: 'white'}} />
                                </div>
                                <div className="col col-12 lg-col-10 md-col-10 sm-col sm-col-12">
                                    <div className="py2" style={{backgroundColor: colors.grey50}}>
                                        {this.props.blockchainIsLoaded ?
                                            <Switch>
                                                <Route
                                                    path={`${WebsitePaths.Portal}/weth`}
                                                    render={this.renderEthWrapper.bind(this)}
                                                />
                                                <Route
                                                    path={`${WebsitePaths.Portal}/fill`}
                                                    render={this.renderFillOrder.bind(this)}
                                                />
                                                <Route
                                                    path={`${WebsitePaths.Portal}/balances`}
                                                    render={this.renderTokenBalances.bind(this)}
                                                />
                                                <Route
                                                    path={`${WebsitePaths.Portal}/trades`}
                                                    component={this.renderTradeHistory.bind(this)}
                                                />
                                                <Route
                                                    path={`${WebsitePaths.Home}`}
                                                    render={this.renderGenerateOrderForm.bind(this)}
                                                />
                                            </Switch> :
                                            <Loading />
                                        }
                                    </div>
                                </div>
                            </div>
                        }
                    </Paper>
                    <BlockchainErrDialog
                        blockchain={this.blockchain}
                        blockchainErr={this.props.blockchainErr}
                        isOpen={this.props.shouldBlockchainErrDialogBeOpen}
                        userAddress={this.props.userAddress}
                        toggleDialogFn={updateShouldBlockchainErrDialogBeOpen}
                        networkId={this.props.networkId}
                    />
                    <WrappedEthSectionNoticeDialog
                        isOpen={this.state.isWethNoticeDialogOpen}
                        onToggleDialog={this.onWethNoticeAccepted.bind(this)}
                    />
                    <PortalDisclaimerDialog
                        isOpen={this.state.isDisclaimerDialogOpen}
                        onToggleDialog={this.onPortalDisclaimerAccepted.bind(this)}
                    />
                    <FlashMessage
                        dispatcher={this.props.dispatcher}
                        flashMessage={this.props.flashMessage}
                    />
                </div>
                <Footer location={this.props.location} />
            </div>
        );
    }
    private renderEthWrapper() {
        return (
            <EthWrappers
                networkId={this.props.networkId}
                blockchain={this.blockchain}
                dispatcher={this.props.dispatcher}
                tokenByAddress={this.props.tokenByAddress}
                tokenStateByAddress={this.props.tokenStateByAddress}
                userAddress={this.props.userAddress}
                userEtherBalance={this.props.userEtherBalance}
            />
        );
    }
    private renderTradeHistory() {
        return (
            <TradeHistory
                tokenByAddress={this.props.tokenByAddress}
                userAddress={this.props.userAddress}
                networkId={this.props.networkId}
            />
        );
    }
    private renderTokenBalances() {
        return (
            <TokenBalances
                blockchain={this.blockchain}
                blockchainErr={this.props.blockchainErr}
                blockchainIsLoaded={this.props.blockchainIsLoaded}
                dispatcher={this.props.dispatcher}
                screenWidth={this.props.screenWidth}
                tokenByAddress={this.props.tokenByAddress}
                tokenStateByAddress={this.props.tokenStateByAddress}
                userAddress={this.props.userAddress}
                userEtherBalance={this.props.userEtherBalance}
                networkId={this.props.networkId}
            />
        );
    }
    private renderFillOrder(match: any, location: Location, history: History) {
        const initialFillOrder = !_.isUndefined(this.props.userSuppliedOrderCache) ?
                                 this.props.userSuppliedOrderCache :
                                 this.sharedOrderIfExists;
        return (
            <FillOrder
                blockchain={this.blockchain}
                blockchainErr={this.props.blockchainErr}
                initialOrder={initialFillOrder}
                isOrderInUrl={!_.isUndefined(this.sharedOrderIfExists)}
                orderFillAmount={this.props.orderFillAmount}
                networkId={this.props.networkId}
                userAddress={this.props.userAddress}
                tokenByAddress={this.props.tokenByAddress}
                tokenStateByAddress={this.props.tokenStateByAddress}
                dispatcher={this.props.dispatcher}
            />
        );
    }
    private renderGenerateOrderForm(match: any, location: Location, history: History) {
        return (
            <GenerateOrderForm
                blockchain={this.blockchain}
                hashData={this.props.hashData}
                dispatcher={this.props.dispatcher}
            />
        );
    }
    private onPortalDisclaimerAccepted() {
        localStorage.setItem(constants.ACCEPT_DISCLAIMER_LOCAL_STORAGE_KEY, 'set');
        this.setState({
            isDisclaimerDialogOpen: false,
        });
    }
    private onWethNoticeAccepted() {
        localStorage.setItem(constants.DISMISS_WETH_NOTICE_LOCAL_STORAGE_KEY, 'set');
        this.setState({
            isWethNoticeDialogOpen: false,
        });
    }
    private getSharedOrderIfExists(): Order {
        const queryString = window.location.search;
        if (queryString.length === 0) {
            return;
        }
        const queryParams = queryString.substring(1).split('&');
        const orderQueryParam = _.find(queryParams, queryParam => {
            const queryPair = queryParam.split('=');
            return queryPair[0] === 'order';
        });
        if (_.isUndefined(orderQueryParam)) {
            return;
        }
        const orderPair = orderQueryParam.split('=');
        if (orderPair.length !== 2) {
            return;
        }

        const validator = new SchemaValidator();
        const order = JSON.parse(decodeURIComponent(orderPair[1]));
        const validationResult = validator.validate(order, orderSchema);
        if (validationResult.errors.length > 0) {
            utils.consoleLog(`Invalid shared order: ${validationResult.errors}`);
            return;
        }
        return order;
    }
    private updateScreenWidth() {
        const newScreenWidth = utils.getScreenWidth();
        this.props.dispatcher.updateScreenWidth(newScreenWidth);
    }
    private async updateBalanceAndAllowanceWithLoadingScreenAsync(tokens: Token[]) {
        this.props.dispatcher.updateBlockchainIsLoaded(false);
        await this.blockchain.updateTokenBalancesAndAllowancesAsync(tokens);
        this.props.dispatcher.updateBlockchainIsLoaded(true);
    }
}