import { BigNumber } from '@0xproject/utils'; import * as _ from 'lodash'; import Paper from 'material-ui/Paper'; 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 { LedgerConfigDialog } from 'ts/components/dialogs/ledger_config_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/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 { State } from 'ts/redux/reducer'; import { orderSchema } from 'ts/schemas/order_schema'; import { SchemaValidator } from 'ts/schemas/validator'; import { BlockchainErrs, HashData, Order, ProviderType, ScreenWidths, Token, TokenByAddress, TokenStateByAddress, WebsitePaths, } from 'ts/types'; import { colors } from 'ts/utils/colors'; 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; injectedProviderName: string; networkId: number; nodeVersion: string; orderFillAmount: BigNumber; providerType: ProviderType; screenWidth: ScreenWidths; tokenByAddress: TokenByAddress; userEtherBalance: BigNumber; userAddress: string; shouldBlockchainErrDialogBeOpen: boolean; userSuppliedOrderCache: Order; location: Location; flashMessage?: string | React.ReactNode; lastForceTokenStateRefetch: number; } interface PortalAllState { prevNetworkId: number; prevNodeVersion: string; prevUserAddress: string; prevPathname: string; isDisclaimerDialogOpen: boolean; isWethNoticeDialogOpen: boolean; isLedgerDialogOpen: boolean; } export class Portal extends React.Component { private _blockchain: Blockchain; private _sharedOrderIfExists: Order; private _throttledScreenWidthUpdate: () => void; public static hasAlreadyDismissedWethNotice() { const didDismissWethNotice = localStorage.getItemIfExists(constants.LOCAL_STORAGE_KEY_DISMISS_WETH_NOTICE); 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.LOCAL_STORAGE_KEY_ACCEPT_DISCLAIMER); 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, isLedgerDialogOpen: false, }; } 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); 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', }; const portalMenuContainerStyle: React.CSSProperties = { overflow: 'hidden', backgroundColor: colors.darkestGrey, color: colors.white, }; return (
{!configs.IS_MAINNET_ENABLED && this.props.networkId === constants.NETWORK_ID_MAINNET ? (
Mainnet unavailable
0x portal is currently unavailable on the Ethereum mainnet.
To try it out, switch to the Kovan test network (networkId: 42).
Check back soon!
) : (
{this.props.blockchainIsLoaded ? ( ) : ( )}
)}
{this.props.blockchainIsLoaded && ( )}
;
); } public onToggleLedgerDialog() { this.setState({ isLedgerDialogOpen: !this.state.isLedgerDialogOpen, }); } private _renderEthWrapper() { return ( ); } private _renderTradeHistory() { return ( ); } private _renderTokenBalances() { const allTokens = _.values(this.props.tokenByAddress); const trackedTokens = _.filter(allTokens, t => t.isTracked); return ( ); } private _renderFillOrder(match: any, location: Location, history: History) { const initialFillOrder = !_.isUndefined(this.props.userSuppliedOrderCache) ? this.props.userSuppliedOrderCache : this._sharedOrderIfExists; return ( ); } private _renderGenerateOrderForm(match: any, location: Location, history: History) { return ( ); } private _onPortalDisclaimerAccepted() { localStorage.setItem(constants.LOCAL_STORAGE_KEY_ACCEPT_DISCLAIMER, 'set'); this.setState({ isDisclaimerDialogOpen: false, }); } private _onWethNoticeAccepted() { localStorage.setItem(constants.LOCAL_STORAGE_KEY_DISMISS_WETH_NOTICE, 'set'); this.setState({ isWethNoticeDialogOpen: false, }); } private _getSharedOrderIfExists(): Order | undefined { const queryString = window.location.search; if (queryString.length === 0) { return undefined; } const queryParams = queryString.substring(1).split('&'); const orderQueryParam = _.find(queryParams, queryParam => { const queryPair = queryParam.split('='); return queryPair[0] === 'order'; }); if (_.isUndefined(orderQueryParam)) { return undefined; } const orderPair = orderQueryParam.split('='); if (orderPair.length !== 2) { return undefined; } 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 undefined; } return order; } private _updateScreenWidth() { const newScreenWidth = utils.getScreenWidth(); this.props.dispatcher.updateScreenWidth(newScreenWidth); } }