739 lines
		
	
	
		
			32 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			739 lines
		
	
	
		
			32 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| import { colors } from '@0xproject/react-shared';
 | |
| import { BigNumber } from '@0xproject/utils';
 | |
| import * as _ from 'lodash';
 | |
| import * as React from 'react';
 | |
| import * as DocumentTitle from 'react-document-title';
 | |
| import { Link, Route, RouteComponentProps, Switch } from 'react-router-dom';
 | |
| 
 | |
| import { Blockchain } from 'ts/blockchain';
 | |
| import { BlockchainErrDialog } from 'ts/components/dialogs/blockchain_err_dialog';
 | |
| import { LedgerConfigDialog } from 'ts/components/dialogs/ledger_config_dialog';
 | |
| import { PortalDisclaimerDialog } from 'ts/components/dialogs/portal_disclaimer_dialog';
 | |
| import { EthWrappers } from 'ts/components/eth_wrappers';
 | |
| import { FillOrder } from 'ts/components/fill_order';
 | |
| import { AssetPicker } from 'ts/components/generate_order/asset_picker';
 | |
| import { MetaTags } from 'ts/components/meta_tags';
 | |
| import { BackButton } from 'ts/components/portal/back_button';
 | |
| import { Loading } from 'ts/components/portal/loading';
 | |
| import { Menu, MenuTheme } from 'ts/components/portal/menu';
 | |
| import { Section } from 'ts/components/portal/section';
 | |
| import { TextHeader } from 'ts/components/portal/text_header';
 | |
| import { RelayerIndex } from 'ts/components/relayer_index/relayer_index';
 | |
| import { TokenBalances } from 'ts/components/token_balances';
 | |
| import { TopBar, TopBarDisplayType } from 'ts/components/top_bar/top_bar';
 | |
| import { TradeHistory } from 'ts/components/trade_history/trade_history';
 | |
| import { Container } from 'ts/components/ui/container';
 | |
| import { FlashMessage } from 'ts/components/ui/flash_message';
 | |
| import { Image } from 'ts/components/ui/image';
 | |
| import { PointerDirection } from 'ts/components/ui/pointer';
 | |
| import { Text } from 'ts/components/ui/text';
 | |
| import { Wallet } from 'ts/components/wallet/wallet';
 | |
| import { GenerateOrderForm } from 'ts/containers/generate_order_form';
 | |
| import { PortalOnboardingFlow } from 'ts/containers/portal_onboarding_flow';
 | |
| import { localStorage } from 'ts/local_storage/local_storage';
 | |
| import { trackedTokenStorage } from 'ts/local_storage/tracked_token_storage';
 | |
| import { FullscreenMessage } from 'ts/pages/fullscreen_message';
 | |
| import { Dispatcher } from 'ts/redux/dispatcher';
 | |
| import { zIndex } from 'ts/style/z_index';
 | |
| import {
 | |
|     BlockchainErrs,
 | |
|     HashData,
 | |
|     ItemByAddress,
 | |
|     Order,
 | |
|     ProviderType,
 | |
|     ScreenWidths,
 | |
|     Token,
 | |
|     TokenByAddress,
 | |
|     TokenStateByAddress,
 | |
|     TokenVisibility,
 | |
|     WebsitePaths,
 | |
| } from 'ts/types';
 | |
| import { analytics } from 'ts/utils/analytics';
 | |
| import { backendClient } from 'ts/utils/backend_client';
 | |
| import { configs } from 'ts/utils/configs';
 | |
| import { constants } from 'ts/utils/constants';
 | |
| import { orderParser } from 'ts/utils/order_parser';
 | |
| import { Translate } from 'ts/utils/translate';
 | |
| import { utils } from 'ts/utils/utils';
 | |
| 
 | |
| export interface PortalProps {
 | |
|     blockchainErr: BlockchainErrs;
 | |
|     blockchainIsLoaded: boolean;
 | |
|     dispatcher: Dispatcher;
 | |
|     hashData: HashData;
 | |
|     injectedProviderName: string;
 | |
|     networkId: number;
 | |
|     nodeVersion: string;
 | |
|     orderFillAmount: BigNumber;
 | |
|     providerType: ProviderType;
 | |
|     screenWidth: ScreenWidths;
 | |
|     tokenByAddress: TokenByAddress;
 | |
|     userEtherBalanceInWei?: BigNumber;
 | |
|     userAddress: string;
 | |
|     shouldBlockchainErrDialogBeOpen: boolean;
 | |
|     userSuppliedOrderCache: Order;
 | |
|     location: Location;
 | |
|     flashMessage?: string | React.ReactNode;
 | |
|     lastForceTokenStateRefetch: number;
 | |
|     translate: Translate;
 | |
|     isPortalOnboardingShowing: boolean;
 | |
|     portalOnboardingStep: number;
 | |
| }
 | |
| 
 | |
| interface PortalState {
 | |
|     prevNetworkId: number;
 | |
|     prevNodeVersion: string;
 | |
|     prevUserAddress: string;
 | |
|     prevPathname: string;
 | |
|     isDisclaimerDialogOpen: boolean;
 | |
|     isLedgerDialogOpen: boolean;
 | |
|     tokenManagementState: TokenManagementState;
 | |
|     trackedTokenStateByAddress: TokenStateByAddress;
 | |
| }
 | |
| 
 | |
| interface AccountManagementItem {
 | |
|     pathName: string;
 | |
|     headerText?: string;
 | |
|     render: () => React.ReactNode;
 | |
| }
 | |
| 
 | |
| enum TokenManagementState {
 | |
|     Add = 'Add',
 | |
|     Remove = 'Remove',
 | |
|     None = 'None',
 | |
| }
 | |
| 
 | |
| const THROTTLE_TIMEOUT = 100;
 | |
| const TOP_BAR_HEIGHT = TopBar.heightForDisplayType(TopBarDisplayType.Expanded);
 | |
| const LEFT_COLUMN_WIDTH = 346;
 | |
| const MENU_PADDING_LEFT = 185;
 | |
| const LARGE_LAYOUT_MAX_WIDTH = 1200;
 | |
| const SIDE_PADDING = 20;
 | |
| const DOCUMENT_TITLE = '0x Portal';
 | |
| const DOCUMENT_DESCRIPTION = 'Learn about and trade on 0x Relayers';
 | |
| 
 | |
| export class Portal extends React.Component<PortalProps, PortalState> {
 | |
|     private _blockchain: Blockchain;
 | |
|     private readonly _sharedOrderIfExists: Order;
 | |
|     private readonly _throttledScreenWidthUpdate: () => void;
 | |
|     constructor(props: PortalProps) {
 | |
|         super(props);
 | |
|         this._sharedOrderIfExists = orderParser.parse(window.location.search);
 | |
|         this._throttledScreenWidthUpdate = _.throttle(this._updateScreenWidth.bind(this), THROTTLE_TIMEOUT);
 | |
|         const didAcceptPortalDisclaimer = localStorage.getItemIfExists(constants.LOCAL_STORAGE_KEY_ACCEPT_DISCLAIMER);
 | |
|         const hasAcceptedDisclaimer =
 | |
|             !_.isUndefined(didAcceptPortalDisclaimer) && !_.isEmpty(didAcceptPortalDisclaimer);
 | |
|         const initialTrackedTokenStateByAddress = this._getInitialTrackedTokenStateByAddress(
 | |
|             this._getCurrentTrackedTokens(),
 | |
|         );
 | |
|         this.state = {
 | |
|             prevNetworkId: this.props.networkId,
 | |
|             prevNodeVersion: this.props.nodeVersion,
 | |
|             prevUserAddress: this.props.userAddress,
 | |
|             prevPathname: this.props.location.pathname,
 | |
|             isDisclaimerDialogOpen: !hasAcceptedDisclaimer,
 | |
|             tokenManagementState: TokenManagementState.None,
 | |
|             isLedgerDialogOpen: false,
 | |
|             trackedTokenStateByAddress: initialTrackedTokenStateByAddress,
 | |
|         };
 | |
|     }
 | |
|     public componentDidMount(): void {
 | |
|         window.addEventListener('resize', this._throttledScreenWidthUpdate);
 | |
|         window.scrollTo(0, 0);
 | |
|     }
 | |
|     public componentWillMount(): void {
 | |
|         this._blockchain = new Blockchain(this.props.dispatcher);
 | |
|     }
 | |
|     public componentWillUnmount(): void {
 | |
|         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, changed user accounts, etc...)
 | |
|         this.props.dispatcher.resetState();
 | |
|     }
 | |
|     public componentDidUpdate(prevProps: PortalProps): void {
 | |
|         if (!prevProps.blockchainIsLoaded && this.props.blockchainIsLoaded) {
 | |
|             // tslint:disable-next-line:no-floating-promises
 | |
|             this._fetchBalancesAndAllowancesAsync(this._getCurrentTrackedTokensAddresses());
 | |
|         }
 | |
|     }
 | |
|     public componentWillReceiveProps(nextProps: PortalProps): void {
 | |
|         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) {
 | |
|             const newUserAddress = _.isEmpty(nextProps.userAddress) ? undefined : nextProps.userAddress;
 | |
|             // tslint:disable-next-line:no-floating-promises
 | |
|             this._blockchain.userAddressUpdatedFireAndForgetAsync(newUserAddress);
 | |
|             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) {
 | |
|             this.setState({
 | |
|                 prevPathname: nextProps.location.pathname,
 | |
|             });
 | |
|         }
 | |
| 
 | |
|         // If the address changed, but the network did not, we can just refetch the currently tracked tokens.
 | |
|         if (
 | |
|             (nextProps.userAddress !== this.props.userAddress && nextProps.networkId === this.props.networkId) ||
 | |
|             nextProps.lastForceTokenStateRefetch !== this.props.lastForceTokenStateRefetch
 | |
|         ) {
 | |
|             // tslint:disable-next-line:no-floating-promises
 | |
|             this._fetchBalancesAndAllowancesAsync(this._getCurrentTrackedTokensAddresses());
 | |
|         }
 | |
| 
 | |
|         const nextTrackedTokens = utils.getTrackedTokens(nextProps.tokenByAddress);
 | |
|         const trackedTokens = this._getCurrentTrackedTokens();
 | |
| 
 | |
|         if (!_.isEqual(nextTrackedTokens, trackedTokens)) {
 | |
|             const newTokens = _.difference(nextTrackedTokens, 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 };
 | |
|             for (const tokenAddress of newTokenAddresses) {
 | |
|                 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(): React.ReactNode {
 | |
|         const updateShouldBlockchainErrDialogBeOpen = this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen.bind(
 | |
|             this.props.dispatcher,
 | |
|         );
 | |
|         const isAssetPickerDialogOpen = this.state.tokenManagementState !== TokenManagementState.None;
 | |
|         const tokenVisibility =
 | |
|             this.state.tokenManagementState === TokenManagementState.Add
 | |
|                 ? TokenVisibility.UNTRACKED
 | |
|                 : TokenVisibility.TRACKED;
 | |
|         return (
 | |
|             <Container>
 | |
|                 <MetaTags title={DOCUMENT_TITLE} description={DOCUMENT_DESCRIPTION} />
 | |
|                 <DocumentTitle title={DOCUMENT_TITLE} />
 | |
|                 <TopBar
 | |
|                     userAddress={this.props.userAddress}
 | |
|                     networkId={this.props.networkId}
 | |
|                     injectedProviderName={this.props.injectedProviderName}
 | |
|                     onToggleLedgerDialog={this._onToggleLedgerDialog.bind(this)}
 | |
|                     dispatcher={this.props.dispatcher}
 | |
|                     providerType={this.props.providerType}
 | |
|                     blockchainIsLoaded={this.props.blockchainIsLoaded}
 | |
|                     location={this.props.location}
 | |
|                     blockchain={this._blockchain}
 | |
|                     translate={this.props.translate}
 | |
|                     displayType={TopBarDisplayType.Expanded}
 | |
|                     style={{
 | |
|                         backgroundColor: colors.lightestGrey,
 | |
|                         position: 'fixed',
 | |
|                         zIndex: zIndex.topBar,
 | |
|                     }}
 | |
|                     maxWidth={LARGE_LAYOUT_MAX_WIDTH}
 | |
|                 />
 | |
|                 <Container marginTop={TOP_BAR_HEIGHT} minHeight="100vh" backgroundColor={colors.lightestGrey}>
 | |
|                     <Switch>
 | |
|                         <Route path={`${WebsitePaths.Portal}/:route`} render={this._renderOtherRoutes.bind(this)} />
 | |
|                         <Route
 | |
|                             exact={true}
 | |
|                             path={`${WebsitePaths.Portal}/`}
 | |
|                             render={this._renderMainRoute.bind(this)}
 | |
|                         />
 | |
|                     </Switch>
 | |
|                     <BlockchainErrDialog
 | |
|                         blockchain={this._blockchain}
 | |
|                         blockchainErr={this.props.blockchainErr}
 | |
|                         isOpen={this.props.shouldBlockchainErrDialogBeOpen}
 | |
|                         userAddress={this.props.userAddress}
 | |
|                         toggleDialogFn={updateShouldBlockchainErrDialogBeOpen}
 | |
|                         networkId={this.props.networkId}
 | |
|                     />
 | |
|                     <FlashMessage dispatcher={this.props.dispatcher} flashMessage={this.props.flashMessage} />
 | |
| 
 | |
|                     <LedgerConfigDialog
 | |
|                         providerType={this.props.providerType}
 | |
|                         networkId={this.props.networkId}
 | |
|                         blockchain={this._blockchain}
 | |
|                         dispatcher={this.props.dispatcher}
 | |
|                         toggleDialogFn={this._onToggleLedgerDialog.bind(this)}
 | |
|                         isOpen={this.state.isLedgerDialogOpen}
 | |
|                     />
 | |
| 
 | |
|                     <AssetPicker
 | |
|                         userAddress={this.props.userAddress}
 | |
|                         networkId={this.props.networkId}
 | |
|                         blockchain={this._blockchain}
 | |
|                         dispatcher={this.props.dispatcher}
 | |
|                         isOpen={isAssetPickerDialogOpen}
 | |
|                         currentTokenAddress={''}
 | |
|                         onTokenChosen={this._onTokenChosen.bind(this)}
 | |
|                         tokenByAddress={this.props.tokenByAddress}
 | |
|                         tokenVisibility={tokenVisibility}
 | |
|                     />
 | |
|                 </Container>
 | |
|             </Container>
 | |
|         );
 | |
|     }
 | |
|     private _renderMainRoute(): React.ReactNode {
 | |
|         if (this._isSmallScreen()) {
 | |
|             return <SmallLayout content={this._renderRelayerIndexSection()} />;
 | |
|         } else {
 | |
|             return <LargeLayout left={this._renderWalletSection()} right={this._renderRelayerIndexSection()} />;
 | |
|         }
 | |
|     }
 | |
|     private _renderOtherRoutes(routeComponentProps: RouteComponentProps<any>): React.ReactNode {
 | |
|         if (this._isSmallScreen()) {
 | |
|             return <SmallLayout content={this._renderAccountManagement()} />;
 | |
|         } else {
 | |
|             return <LargeLayout left={this._renderMenu(routeComponentProps)} right={this._renderAccountManagement()} />;
 | |
|         }
 | |
|     }
 | |
|     private _renderMenu(routeComponentProps: RouteComponentProps<any>): React.ReactNode {
 | |
|         const menuTheme: MenuTheme = {
 | |
|             paddingLeft: MENU_PADDING_LEFT,
 | |
|             textColor: colors.darkerGrey,
 | |
|             iconColor: colors.darkerGrey,
 | |
|             selectedIconColor: colors.yellow800,
 | |
|             selectedBackgroundColor: 'transparent',
 | |
|         };
 | |
|         return (
 | |
|             <Section
 | |
|                 header={<BackButton to={`${WebsitePaths.Portal}`} labelText="back to Relayers" />}
 | |
|                 body={<Menu selectedPath={routeComponentProps.location.pathname} theme={menuTheme} />}
 | |
|             />
 | |
|         );
 | |
|     }
 | |
|     private _renderWallet(): React.ReactNode {
 | |
|         const isMobile = utils.isMobileWidth(this.props.screenWidth);
 | |
|         // We need room to scroll down for mobile onboarding
 | |
|         const marginBottom = isMobile ? '250px' : '15px';
 | |
|         return (
 | |
|             <div>
 | |
|                 <Container className="flex flex-column items-center">
 | |
|                     {isMobile && (
 | |
|                         <Container marginTop="20px" marginBottom="20px">
 | |
|                             {this._renderStartOnboarding()}
 | |
|                         </Container>
 | |
|                     )}
 | |
|                     <Container marginBottom={marginBottom} width="100%">
 | |
|                         <Wallet
 | |
|                             style={
 | |
|                                 !isMobile && this.props.isPortalOnboardingShowing
 | |
|                                     ? { zIndex: zIndex.aboveOverlay, position: 'relative' }
 | |
|                                     : undefined
 | |
|                             }
 | |
|                             userAddress={this.props.userAddress}
 | |
|                             networkId={this.props.networkId}
 | |
|                             blockchain={this._blockchain}
 | |
|                             blockchainIsLoaded={this.props.blockchainIsLoaded}
 | |
|                             blockchainErr={this.props.blockchainErr}
 | |
|                             dispatcher={this.props.dispatcher}
 | |
|                             tokenByAddress={this.props.tokenByAddress}
 | |
|                             trackedTokens={this._getCurrentTrackedTokens()}
 | |
|                             userEtherBalanceInWei={this.props.userEtherBalanceInWei}
 | |
|                             lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
 | |
|                             injectedProviderName={this.props.injectedProviderName}
 | |
|                             providerType={this.props.providerType}
 | |
|                             screenWidth={this.props.screenWidth}
 | |
|                             location={this.props.location}
 | |
|                             trackedTokenStateByAddress={this.state.trackedTokenStateByAddress}
 | |
|                             onToggleLedgerDialog={this._onToggleLedgerDialog.bind(this)}
 | |
|                             onAddToken={this._onAddToken.bind(this)}
 | |
|                             onRemoveToken={this._onRemoveToken.bind(this)}
 | |
|                             refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this)}
 | |
|                             toggleTooltipDirection={
 | |
|                                 this.props.isPortalOnboardingShowing ? PointerDirection.Left : PointerDirection.Right
 | |
|                             }
 | |
|                         />
 | |
|                     </Container>
 | |
|                     {!isMobile && <Container marginTop="8px">{this._renderStartOnboarding()}</Container>}
 | |
|                 </Container>
 | |
|                 <PortalOnboardingFlow
 | |
|                     blockchain={this._blockchain}
 | |
|                     trackedTokenStateByAddress={this.state.trackedTokenStateByAddress}
 | |
|                     refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this)}
 | |
|                 />
 | |
|             </div>
 | |
|         );
 | |
|     }
 | |
|     private _renderStartOnboarding(): React.ReactNode {
 | |
|         const isMobile = utils.isMobileWidth(this.props.screenWidth);
 | |
|         const shouldStartOnboarding = !isMobile || this.props.location.pathname === `${WebsitePaths.Portal}/account`;
 | |
|         const startOnboarding = (
 | |
|             <Container className="flex items-center center">
 | |
|                 <Text fontColor={colors.mediumBlue} fontSize="16px" onClick={this._startOnboarding.bind(this)}>
 | |
|                     Set up your account to start trading
 | |
|                 </Text>
 | |
|                 <Container marginLeft="8px" paddingTop="3px">
 | |
|                     <Image src="/images/setup_account_icon.svg" height="20px" width="20x" />
 | |
|                 </Container>
 | |
|             </Container>
 | |
|         );
 | |
|         return !shouldStartOnboarding ? (
 | |
|             <Link to={{ pathname: `${WebsitePaths.Portal}/account` }} style={{ textDecoration: 'none' }}>
 | |
|                 {startOnboarding}
 | |
|             </Link>
 | |
|         ) : (
 | |
|             startOnboarding
 | |
|         );
 | |
|     }
 | |
|     private _startOnboarding(): void {
 | |
|         analytics.track('Onboarding Started', {
 | |
|             reason: 'manual',
 | |
|             stepIndex: this.props.portalOnboardingStep,
 | |
|         });
 | |
|         this.props.dispatcher.updatePortalOnboardingShowing(true);
 | |
|     }
 | |
|     private _renderWalletSection(): React.ReactNode {
 | |
|         return <Section header={<TextHeader labelText="Your Account" />} body={this._renderWallet()} />;
 | |
|     }
 | |
|     private _renderAccountManagement(): React.ReactNode {
 | |
|         const accountManagementItems: AccountManagementItem[] = [
 | |
|             {
 | |
|                 pathName: `${WebsitePaths.Portal}/weth`,
 | |
|                 headerText: 'Wrapped ETH',
 | |
|                 render: this._renderEthWrapper.bind(this),
 | |
|             },
 | |
|             {
 | |
|                 pathName: `${WebsitePaths.Portal}/account`,
 | |
|                 headerText: this._isSmallScreen() ? undefined : 'Your Account',
 | |
|                 render: this._isSmallScreen() ? this._renderWallet.bind(this) : this._renderTokenBalances.bind(this),
 | |
|             },
 | |
|             {
 | |
|                 pathName: `${WebsitePaths.Portal}/trades`,
 | |
|                 headerText: 'Trade History',
 | |
|                 render: this._renderTradeHistory.bind(this),
 | |
|             },
 | |
|             {
 | |
|                 pathName: `${WebsitePaths.Portal}/generate`,
 | |
|                 headerText: 'Generate Order',
 | |
|                 render: this._renderGenerateOrderForm.bind(this),
 | |
|             },
 | |
|             {
 | |
|                 pathName: `${WebsitePaths.Portal}/fill`,
 | |
|                 headerText: 'Fill Order',
 | |
|                 render: this._renderFillOrder.bind(this),
 | |
|             },
 | |
|         ];
 | |
|         return (
 | |
|             <div>
 | |
|                 <Switch>
 | |
|                     {_.map(accountManagementItems, item => {
 | |
|                         return (
 | |
|                             <Route
 | |
|                                 key={item.pathName}
 | |
|                                 path={item.pathName}
 | |
|                                 render={this._renderAccountManagementItem.bind(this, item)}
 | |
|                             />
 | |
|                         );
 | |
|                     })}}
 | |
|                     <Route render={this._renderNotFoundMessage.bind(this)} />
 | |
|                 </Switch>
 | |
|                 <PortalDisclaimerDialog
 | |
|                     isOpen={this.state.isDisclaimerDialogOpen}
 | |
|                     onToggleDialog={this._onPortalDisclaimerAccepted.bind(this)}
 | |
|                 />
 | |
|             </div>
 | |
|         );
 | |
|     }
 | |
|     private _renderAccountManagementItem(item: AccountManagementItem): React.ReactNode {
 | |
|         return (
 | |
|             <Section
 | |
|                 header={!_.isUndefined(item.headerText) && <TextHeader labelText={item.headerText} />}
 | |
|                 body={<Loading isLoading={!this.props.blockchainIsLoaded} content={item.render()} />}
 | |
|             />
 | |
|         );
 | |
|     }
 | |
|     private _renderEthWrapper(): React.ReactNode {
 | |
|         return (
 | |
|             <EthWrappers
 | |
|                 networkId={this.props.networkId}
 | |
|                 blockchain={this._blockchain}
 | |
|                 dispatcher={this.props.dispatcher}
 | |
|                 tokenByAddress={this.props.tokenByAddress}
 | |
|                 userAddress={this.props.userAddress}
 | |
|                 userEtherBalanceInWei={this.props.userEtherBalanceInWei}
 | |
|                 lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
 | |
|                 isFullWidth={true}
 | |
|             />
 | |
|         );
 | |
|     }
 | |
|     private _renderTradeHistory(): React.ReactNode {
 | |
|         return (
 | |
|             <TradeHistory
 | |
|                 tokenByAddress={this.props.tokenByAddress}
 | |
|                 userAddress={this.props.userAddress}
 | |
|                 networkId={this.props.networkId}
 | |
|                 isFullWidth={true}
 | |
|                 shouldHideHeader={true}
 | |
|                 isScrollable={false}
 | |
|             />
 | |
|         );
 | |
|     }
 | |
|     private _renderGenerateOrderForm(): React.ReactNode {
 | |
|         return (
 | |
|             <GenerateOrderForm
 | |
|                 blockchain={this._blockchain}
 | |
|                 hashData={this.props.hashData}
 | |
|                 dispatcher={this.props.dispatcher}
 | |
|                 isFullWidth={true}
 | |
|                 shouldHideHeader={true}
 | |
|             />
 | |
|         );
 | |
|     }
 | |
|     private _renderFillOrder(): React.ReactNode {
 | |
|         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}
 | |
|                 dispatcher={this.props.dispatcher}
 | |
|                 lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
 | |
|                 isFullWidth={true}
 | |
|                 shouldHideHeader={true}
 | |
|             />
 | |
|         );
 | |
|     }
 | |
|     private _renderTokenBalances(): React.ReactNode {
 | |
|         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}
 | |
|                 trackedTokens={this._getCurrentTrackedTokens()}
 | |
|                 userAddress={this.props.userAddress}
 | |
|                 userEtherBalanceInWei={this.props.userEtherBalanceInWei}
 | |
|                 networkId={this.props.networkId}
 | |
|                 lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
 | |
|                 isFullWidth={true}
 | |
|             />
 | |
|         );
 | |
|     }
 | |
|     private _renderRelayerIndexSection(): React.ReactNode {
 | |
|         const isMobile = utils.isMobileWidth(this.props.screenWidth);
 | |
|         return (
 | |
|             <Section
 | |
|                 header={!isMobile && <TextHeader labelText="0x Relayers" />}
 | |
|                 body={
 | |
|                     <Container className="flex flex-column items-center">
 | |
|                         {isMobile && (
 | |
|                             <Container marginTop="20px" marginBottom="20px">
 | |
|                                 {this._renderStartOnboarding()}
 | |
|                             </Container>
 | |
|                         )}
 | |
|                         <RelayerIndex networkId={this.props.networkId} screenWidth={this.props.screenWidth} />
 | |
|                     </Container>
 | |
|                 }
 | |
|             />
 | |
|         );
 | |
|     }
 | |
|     private _renderNotFoundMessage(): React.ReactNode {
 | |
|         return (
 | |
|             <FullscreenMessage
 | |
|                 headerText="404 Not Found"
 | |
|                 bodyText="Hm... looks like we couldn't find what you are looking for."
 | |
|             />
 | |
|         );
 | |
|     }
 | |
|     private _onTokenChosen(tokenAddress: string): void {
 | |
|         if (_.isEmpty(tokenAddress)) {
 | |
|             this.setState({
 | |
|                 tokenManagementState: TokenManagementState.None,
 | |
|             });
 | |
|             return;
 | |
|         }
 | |
|         const token = this.props.tokenByAddress[tokenAddress];
 | |
|         const isDefaultTrackedToken = _.includes(configs.DEFAULT_TRACKED_TOKEN_SYMBOLS, token.symbol);
 | |
|         if (this.state.tokenManagementState === TokenManagementState.Remove && !isDefaultTrackedToken) {
 | |
|             if (token.isRegistered) {
 | |
|                 // Remove the token from tracked tokens
 | |
|                 const newToken: Token = {
 | |
|                     ...token,
 | |
|                     trackedTimestamp: undefined,
 | |
|                 };
 | |
|                 this.props.dispatcher.updateTokenByAddress([newToken]);
 | |
|             } else {
 | |
|                 this.props.dispatcher.removeTokenToTokenByAddress(token);
 | |
|             }
 | |
|             trackedTokenStorage.removeTrackedToken(this.props.userAddress, this.props.networkId, tokenAddress);
 | |
|         } else if (isDefaultTrackedToken) {
 | |
|             this.props.dispatcher.showFlashMessage(`Cannot remove ${token.name} because it's a default token`);
 | |
|         }
 | |
|         this.setState({
 | |
|             tokenManagementState: TokenManagementState.None,
 | |
|         });
 | |
|     }
 | |
|     private _onToggleLedgerDialog(): void {
 | |
|         this.setState({
 | |
|             isLedgerDialogOpen: !this.state.isLedgerDialogOpen,
 | |
|         });
 | |
|     }
 | |
|     private _onAddToken(): void {
 | |
|         this.setState({
 | |
|             tokenManagementState: TokenManagementState.Add,
 | |
|         });
 | |
|     }
 | |
|     private _onRemoveToken(): void {
 | |
|         this.setState({
 | |
|             tokenManagementState: TokenManagementState.Remove,
 | |
|         });
 | |
|     }
 | |
|     private _onPortalDisclaimerAccepted(): void {
 | |
|         localStorage.setItem(constants.LOCAL_STORAGE_KEY_ACCEPT_DISCLAIMER, 'set');
 | |
|         this.setState({
 | |
|             isDisclaimerDialogOpen: false,
 | |
|         });
 | |
|     }
 | |
|     private _updateScreenWidth(): void {
 | |
|         const newScreenWidth = utils.getScreenWidth();
 | |
|         this.props.dispatcher.updateScreenWidth(newScreenWidth);
 | |
|     }
 | |
|     private _isSmallScreen(): boolean {
 | |
|         const isSmallScreen = this.props.screenWidth === ScreenWidths.Sm;
 | |
|         return isSmallScreen;
 | |
|     }
 | |
|     private _getCurrentTrackedTokens(): Token[] {
 | |
|         return utils.getTrackedTokens(this.props.tokenByAddress);
 | |
|     }
 | |
|     private _getCurrentTrackedTokensAddresses(): string[] {
 | |
|         return _.map(this._getCurrentTrackedTokens(), token => token.address);
 | |
|     }
 | |
|     private _getInitialTrackedTokenStateByAddress(trackedTokens: Token[]): TokenStateByAddress {
 | |
|         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[]): Promise<void> {
 | |
|         const trackedTokenStateByAddress = this.state.trackedTokenStateByAddress;
 | |
|         const userAddressIfExists = _.isEmpty(this.props.userAddress) ? undefined : this.props.userAddress;
 | |
|         const balancesAndAllowances = await Promise.all(
 | |
|             tokenAddresses.map(async tokenAddress => {
 | |
|                 return this._blockchain.getTokenBalanceAndAllowanceAsync(userAddressIfExists, tokenAddress);
 | |
|             }),
 | |
|         );
 | |
|         const priceByAddress = await this._getPriceByAddressAsync(tokenAddresses);
 | |
|         for (let i = 0; i < tokenAddresses.length; i++) {
 | |
|             // Order is preserved in Promise.all
 | |
|             const [balance, allowance] = balancesAndAllowances[i];
 | |
|             const tokenAddress = tokenAddresses[i];
 | |
|             trackedTokenStateByAddress[tokenAddress] = {
 | |
|                 balance,
 | |
|                 allowance,
 | |
|                 isLoaded: true,
 | |
|                 price: priceByAddress[tokenAddress],
 | |
|             };
 | |
|         }
 | |
|         this.setState({
 | |
|             trackedTokenStateByAddress,
 | |
|         });
 | |
|     }
 | |
| 
 | |
|     private async _getPriceByAddressAsync(tokenAddresses: string[]): Promise<ItemByAddress<BigNumber>> {
 | |
|         if (_.isEmpty(tokenAddresses)) {
 | |
|             return {};
 | |
|         }
 | |
|         // for each input token address, search for the corresponding symbol in this.props.tokenByAddress, if it exists
 | |
|         // create a mapping from existing symbols -> address
 | |
|         const tokenAddressBySymbol: { [symbol: string]: string } = {};
 | |
|         _.each(tokenAddresses, address => {
 | |
|             const tokenIfExists = _.get(this.props.tokenByAddress, address);
 | |
|             if (!_.isUndefined(tokenIfExists)) {
 | |
|                 const symbol = tokenIfExists.symbol;
 | |
|                 tokenAddressBySymbol[symbol] = address;
 | |
|             }
 | |
|         });
 | |
|         const tokenSymbols = _.keys(tokenAddressBySymbol);
 | |
|         try {
 | |
|             const priceBySymbol = await backendClient.getPriceInfoAsync(tokenSymbols);
 | |
|             const priceByAddress = _.mapKeys(priceBySymbol, (_value, symbol) => _.get(tokenAddressBySymbol, symbol));
 | |
|             const result = _.mapValues(priceByAddress, price => {
 | |
|                 const priceBigNumber = new BigNumber(price);
 | |
|                 return priceBigNumber;
 | |
|             });
 | |
|             return result;
 | |
|         } catch (err) {
 | |
|             return {};
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     private async _refetchTokenStateAsync(tokenAddress: string): Promise<void> {
 | |
|         await this._fetchBalancesAndAllowancesAsync([tokenAddress]);
 | |
|     }
 | |
| }
 | |
| 
 | |
| interface LargeLayoutProps {
 | |
|     left: React.ReactNode;
 | |
|     right: React.ReactNode;
 | |
| }
 | |
| const LargeLayout = (props: LargeLayoutProps) => {
 | |
|     return (
 | |
|         <Container
 | |
|             className="mx-auto flex flex-center"
 | |
|             maxWidth={LARGE_LAYOUT_MAX_WIDTH}
 | |
|             paddingLeft={SIDE_PADDING}
 | |
|             paddingRight={SIDE_PADDING}
 | |
|         >
 | |
|             <div className="flex-last">
 | |
|                 <Container width={LEFT_COLUMN_WIDTH} position="fixed" zIndex={zIndex.aboveTopBar}>
 | |
|                     {props.left}
 | |
|                 </Container>
 | |
|             </div>
 | |
|             <Container className="flex-auto" marginLeft={LEFT_COLUMN_WIDTH}>
 | |
|                 <Container className="flex-auto" marginLeft={SIDE_PADDING}>
 | |
|                     {props.right}
 | |
|                 </Container>
 | |
|             </Container>
 | |
|         </Container>
 | |
|     );
 | |
| };
 | |
| 
 | |
| interface SmallLayoutProps {
 | |
|     content: React.ReactNode;
 | |
| }
 | |
| const SmallLayout = (props: SmallLayoutProps) => {
 | |
|     return (
 | |
|         <div className="flex flex-center">
 | |
|             <Container className="flex-auto" paddingLeft={SIDE_PADDING} paddingRight={SIDE_PADDING}>
 | |
|                 {props.content}
 | |
|             </Container>
 | |
|         </div>
 | |
|     );
 | |
| }; // tslint:disable:max-file-line-count
 |