472 lines
20 KiB
TypeScript
472 lines
20 KiB
TypeScript
import { colors, Styles } 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 { PortalMenu } from 'ts/components/portal/portal_menu';
|
|
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 { FlashMessage } from 'ts/components/ui/flash_message';
|
|
import { Wallet } from 'ts/components/wallet/wallet';
|
|
import { GenerateOrderForm } from 'ts/containers/generate_order_form';
|
|
import { localStorage } from 'ts/local_storage/local_storage';
|
|
import { trackedTokenStorage } from 'ts/local_storage/tracked_token_storage';
|
|
import { Dispatcher } from 'ts/redux/dispatcher';
|
|
import {
|
|
BlockchainErrs,
|
|
HashData,
|
|
Order,
|
|
ProviderType,
|
|
ScreenWidths,
|
|
TokenByAddress,
|
|
TokenVisibility,
|
|
WebsitePaths,
|
|
} from 'ts/types';
|
|
import { configs } from 'ts/utils/configs';
|
|
import { constants } from 'ts/utils/constants';
|
|
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;
|
|
}
|
|
|
|
interface PortalState {
|
|
prevNetworkId: number;
|
|
prevNodeVersion: string;
|
|
prevUserAddress: string;
|
|
prevPathname: string;
|
|
isDisclaimerDialogOpen: boolean;
|
|
isLedgerDialogOpen: boolean;
|
|
tokenManagementState: TokenManagementState;
|
|
}
|
|
|
|
enum TokenManagementState {
|
|
Add = 'Add',
|
|
Remove = 'Remove',
|
|
None = 'None',
|
|
}
|
|
|
|
const THROTTLE_TIMEOUT = 100;
|
|
const TOP_BAR_HEIGHT = TopBar.heightForDisplayType(TopBarDisplayType.Expanded);
|
|
const BACK_BUTTON_HEIGHT = 28;
|
|
|
|
const styles: Styles = {
|
|
root: {
|
|
width: '100%',
|
|
height: '100%',
|
|
backgroundColor: colors.lightestGrey,
|
|
},
|
|
body: {
|
|
height: `calc(100vh - ${TOP_BAR_HEIGHT}px)`,
|
|
},
|
|
scrollContainer: {
|
|
height: `calc(100vh - ${TOP_BAR_HEIGHT}px)`,
|
|
WebkitOverflowScrolling: 'touch',
|
|
overflow: 'auto',
|
|
},
|
|
title: {
|
|
fontWeight: 'bold',
|
|
fontSize: 20,
|
|
},
|
|
backButton: {
|
|
height: BACK_BUTTON_HEIGHT,
|
|
backgroundColor: colors.white,
|
|
borderRadius: BACK_BUTTON_HEIGHT,
|
|
boxShadow: `0px 4px 6px ${colors.walletBoxShadow}`,
|
|
},
|
|
backButtonIcon: {
|
|
color: colors.mediumBlue,
|
|
fontSize: 20,
|
|
},
|
|
};
|
|
|
|
export class Portal extends React.Component<PortalProps, PortalState> {
|
|
private _blockchain: Blockchain;
|
|
private _sharedOrderIfExists: Order;
|
|
private _throttledScreenWidthUpdate: () => void;
|
|
constructor(props: PortalProps) {
|
|
super(props);
|
|
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);
|
|
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,
|
|
};
|
|
}
|
|
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 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,
|
|
});
|
|
}
|
|
}
|
|
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 (
|
|
<div style={styles.root}>
|
|
<DocumentTitle title="0x Portal DApp" />
|
|
<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 }}
|
|
/>
|
|
<div id="portal" style={styles.body}>
|
|
<div className="sm-flex flex-center">
|
|
<div className="flex-last px3">
|
|
<div style={{ width: 346 }}>
|
|
<Switch>
|
|
<Route
|
|
path={`${WebsitePaths.Portal}/:route`}
|
|
render={this._renderMenu.bind(this)}
|
|
/>
|
|
<Route
|
|
exact={true}
|
|
path={`${WebsitePaths.Portal}`}
|
|
render={this._renderWallet.bind(this)}
|
|
/>
|
|
</Switch>
|
|
</div>
|
|
</div>
|
|
<div className="flex-auto px3" style={styles.scrollContainer}>
|
|
<Switch>
|
|
<Route
|
|
path={`${WebsitePaths.Portal}/weth`}
|
|
render={this._renderEthWrapper.bind(this)}
|
|
/>
|
|
<Route
|
|
path={`${WebsitePaths.Portal}/account`}
|
|
render={this._renderTokenBalances.bind(this)}
|
|
/>
|
|
<Route
|
|
path={`${WebsitePaths.Portal}/trades`}
|
|
render={this._renderTradeHistory.bind(this)}
|
|
/>
|
|
<Route
|
|
path={`${WebsitePaths.Portal}/direct`}
|
|
render={this._renderTradeDirect.bind(this)}
|
|
/>
|
|
<Route
|
|
exact={true}
|
|
path={`${WebsitePaths.Portal}/`}
|
|
render={this._renderRelayerIndex.bind(this)}
|
|
/>
|
|
</Switch>
|
|
</div>
|
|
</div>
|
|
<BlockchainErrDialog
|
|
blockchain={this._blockchain}
|
|
blockchainErr={this.props.blockchainErr}
|
|
isOpen={this.props.shouldBlockchainErrDialogBeOpen}
|
|
userAddress={this.props.userAddress}
|
|
toggleDialogFn={updateShouldBlockchainErrDialogBeOpen}
|
|
networkId={this.props.networkId}
|
|
/>
|
|
<PortalDisclaimerDialog
|
|
isOpen={this.state.isDisclaimerDialogOpen}
|
|
onToggleDialog={this._onPortalDisclaimerAccepted.bind(this)}
|
|
/>
|
|
<FlashMessage dispatcher={this.props.dispatcher} flashMessage={this.props.flashMessage} />
|
|
{this.props.blockchainIsLoaded && (
|
|
<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}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
private _renderMenu(routeComponentProps: RouteComponentProps<any>): React.ReactNode {
|
|
return (
|
|
<div>
|
|
<BackButton to={`${WebsitePaths.Portal}`} labelText={'back to Relayers'} />
|
|
<PortalMenu selectedPath={routeComponentProps.location.pathname} />
|
|
</div>
|
|
);
|
|
}
|
|
private _renderWallet(): React.ReactNode {
|
|
const allTokens = _.values(this.props.tokenByAddress);
|
|
const trackedTokens = _.filter(allTokens, t => t.isTracked);
|
|
return (
|
|
<div>
|
|
<Title labelText={'Your Account'} />
|
|
<Wallet
|
|
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={trackedTokens}
|
|
userEtherBalanceInWei={this.props.userEtherBalanceInWei}
|
|
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
|
|
injectedProviderName={this.props.injectedProviderName}
|
|
providerType={this.props.providerType}
|
|
onToggleLedgerDialog={this._onToggleLedgerDialog.bind(this)}
|
|
onAddToken={this._onAddToken.bind(this)}
|
|
onRemoveToken={this._onRemoveToken.bind(this)}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
private _renderEthWrapper(): React.ReactNode {
|
|
return (
|
|
<div>
|
|
<Title labelText={'Wrapped ETH'} />
|
|
<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}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
private _renderTradeHistory(): React.ReactNode {
|
|
return (
|
|
<div>
|
|
<Title labelText={'Trade History'} />
|
|
<TradeHistory
|
|
tokenByAddress={this.props.tokenByAddress}
|
|
userAddress={this.props.userAddress}
|
|
networkId={this.props.networkId}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
private _renderTradeDirect(match: any, location: Location, history: History): React.ReactNode {
|
|
return (
|
|
<div>
|
|
<Title labelText={'Trade Direct'} />
|
|
<GenerateOrderForm
|
|
blockchain={this._blockchain}
|
|
hashData={this.props.hashData}
|
|
dispatcher={this.props.dispatcher}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
private _renderTokenBalances(): React.ReactNode {
|
|
const allTokens = _.values(this.props.tokenByAddress);
|
|
const trackedTokens = _.filter(allTokens, t => t.isTracked);
|
|
return (
|
|
<div>
|
|
<Title labelText={'Your Account'} />
|
|
<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={trackedTokens}
|
|
userAddress={this.props.userAddress}
|
|
userEtherBalanceInWei={this.props.userEtherBalanceInWei}
|
|
networkId={this.props.networkId}
|
|
lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
private _renderRelayerIndex(): React.ReactNode {
|
|
return (
|
|
<div>
|
|
<Title labelText={'Explore 0x Relayers'} />
|
|
<RelayerIndex networkId={this.props.networkId} />
|
|
</div>
|
|
);
|
|
}
|
|
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,
|
|
isTracked: false,
|
|
};
|
|
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);
|
|
}
|
|
}
|
|
|
|
interface TitleProps {
|
|
labelText: string;
|
|
}
|
|
const Title = (props: TitleProps) => {
|
|
return (
|
|
<div className="py3" style={styles.title}>
|
|
{props.labelText}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
interface BackButtonProps {
|
|
to: string;
|
|
labelText: string;
|
|
}
|
|
const BackButton = (props: BackButtonProps) => {
|
|
return (
|
|
<div style={{ height: 65, paddingTop: 25 }}>
|
|
<Link to={props.to} style={{ textDecoration: 'none' }}>
|
|
<div className="flex right" style={{ ...styles.backButton, paddingTop: 10 }}>
|
|
<div style={{ marginLeft: 12 }}>
|
|
<i style={styles.backButtonIcon} className={`zmdi zmdi-arrow-left`} />
|
|
</div>
|
|
<div style={{ marginLeft: 12, marginRight: 12 }}>
|
|
<div style={{ fontSize: 16, color: colors.lightGrey }}>{props.labelText}</div>
|
|
</div>
|
|
</div>
|
|
</Link>
|
|
</div>
|
|
);
|
|
};
|