Initial Ledger support implementation
This commit is contained in:
@@ -8,9 +8,12 @@
|
||||
"clean": "shx rm -f public/bundle*",
|
||||
"lint": "tslint --project . 'ts/**/*.ts' 'ts/**/*.tsx'",
|
||||
"dev": "webpack-dev-server --content-base public --https",
|
||||
"update_contracts": "for i in ${npm_package_config_artifacts}; do copyfiles -u 4 ../contracts/build/contracts/$i.json ../website/contracts; done;",
|
||||
"deploy_staging": "npm run build; aws s3 sync ./public/. s3://staging-0xproject --profile 0xproject --region us-east-1 --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers",
|
||||
"deploy_live": "npm run build; aws s3 sync ./public/. s3://0xproject.com --profile 0xproject --region us-east-1 --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers"
|
||||
"update_contracts":
|
||||
"for i in ${npm_package_config_artifacts}; do copyfiles -u 4 ../contracts/build/contracts/$i.json ../website/contracts; done;",
|
||||
"deploy_staging":
|
||||
"npm run build; aws s3 sync ./public/. s3://staging-0xproject --profile 0xproject --region us-east-1 --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers",
|
||||
"deploy_live":
|
||||
"npm run build; aws s3 sync ./public/. s3://0xproject.com --profile 0xproject --region us-east-1 --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers"
|
||||
},
|
||||
"config": {
|
||||
"artifacts": "Mintable"
|
||||
|
||||
BIN
packages/website/public/images/ledger_icon.png
Normal file
BIN
packages/website/public/images/ledger_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.8 KiB |
BIN
packages/website/public/images/metamask_or_parity.png
Normal file
BIN
packages/website/public/images/metamask_or_parity.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
BIN
packages/website/public/images/network_icons/kovan.png
Normal file
BIN
packages/website/public/images/network_icons/kovan.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 244 B |
BIN
packages/website/public/images/network_icons/mainnet.png
Normal file
BIN
packages/website/public/images/network_icons/mainnet.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 205 B |
BIN
packages/website/public/images/network_icons/rinkby.png
Normal file
BIN
packages/website/public/images/network_icons/rinkby.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 126 B |
BIN
packages/website/public/images/network_icons/ropsten.png
Normal file
BIN
packages/website/public/images/network_icons/ropsten.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 251 B |
@@ -64,6 +64,7 @@ export class Blockchain {
|
||||
private _exchangeAddress: string;
|
||||
private _userAddress: string;
|
||||
private _cachedProvider: Web3.Provider;
|
||||
private _cachedProviderNetworkId: number;
|
||||
private _ledgerSubprovider: LedgerWalletSubprovider;
|
||||
private _zrxPollIntervalId: NodeJS.Timer;
|
||||
private static async _onPageLoadAsync(): Promise<void> {
|
||||
@@ -133,14 +134,14 @@ export class Blockchain {
|
||||
} else if (this.networkId !== newNetworkId) {
|
||||
this.networkId = newNetworkId;
|
||||
this._dispatcher.encounteredBlockchainError(BlockchainErrs.NoError);
|
||||
await this._fetchTokenInformationAsync();
|
||||
await this.fetchTokenInformationAsync();
|
||||
await this._rehydrateStoreWithContractEvents();
|
||||
}
|
||||
}
|
||||
public async userAddressUpdatedFireAndForgetAsync(newUserAddress: string) {
|
||||
if (this._userAddress !== newUserAddress) {
|
||||
this._userAddress = newUserAddress;
|
||||
await this._fetchTokenInformationAsync();
|
||||
await this.fetchTokenInformationAsync();
|
||||
await this._rehydrateStoreWithContractEvents();
|
||||
}
|
||||
}
|
||||
@@ -180,63 +181,62 @@ export class Blockchain {
|
||||
}
|
||||
this._ledgerSubprovider.setPathIndex(pathIndex);
|
||||
}
|
||||
public async providerTypeUpdatedFireAndForgetAsync(providerType: ProviderType) {
|
||||
public async updateProviderToLedgerAsync(networkId: number) {
|
||||
utils.assert(!_.isUndefined(this._zeroEx), 'ZeroEx must be instantiated.');
|
||||
// Should actually be Web3.Provider|ProviderEngine union type but it causes issues
|
||||
// later on in the logic.
|
||||
let provider;
|
||||
switch (providerType) {
|
||||
case ProviderType.Ledger: {
|
||||
const isU2FSupported = await utils.isU2FSupportedAsync();
|
||||
if (!isU2FSupported) {
|
||||
throw new Error('Cannot update providerType to LEDGER without U2F support');
|
||||
}
|
||||
|
||||
// Cache injected provider so that we can switch the user back to it easily
|
||||
this._cachedProvider = this._web3Wrapper.getProviderObj();
|
||||
|
||||
this._dispatcher.updateUserAddress(''); // Clear old userAddress
|
||||
|
||||
provider = new ProviderEngine();
|
||||
const ledgerWalletConfigs = {
|
||||
networkId: this.networkId,
|
||||
ledgerEthereumClientFactoryAsync: ledgerEthereumBrowserClientFactoryAsync,
|
||||
};
|
||||
this._ledgerSubprovider = new LedgerSubprovider(ledgerWalletConfigs);
|
||||
provider.addProvider(this._ledgerSubprovider);
|
||||
provider.addProvider(new FilterSubprovider());
|
||||
const networkId = configs.IS_MAINNET_ENABLED
|
||||
? constants.NETWORK_ID_MAINNET
|
||||
: constants.NETWORK_ID_TESTNET;
|
||||
provider.addProvider(new RedundantRPCSubprovider(configs.PUBLIC_NODE_URLS_BY_NETWORK_ID[networkId]));
|
||||
provider.start();
|
||||
this._web3Wrapper.destroy();
|
||||
const shouldPollUserAddress = false;
|
||||
this._web3Wrapper = new Web3Wrapper(this._dispatcher, provider, this.networkId, shouldPollUserAddress);
|
||||
this._zeroEx.setProvider(provider, networkId);
|
||||
await this._postInstantiationOrUpdatingProviderZeroExAsync();
|
||||
break;
|
||||
}
|
||||
|
||||
case ProviderType.Injected: {
|
||||
if (_.isUndefined(this._cachedProvider)) {
|
||||
return; // Going from injected to injected, so we noop
|
||||
}
|
||||
provider = this._cachedProvider;
|
||||
const shouldPollUserAddress = true;
|
||||
this._web3Wrapper = new Web3Wrapper(this._dispatcher, provider, this.networkId, shouldPollUserAddress);
|
||||
this._zeroEx.setProvider(provider, this.networkId);
|
||||
await this._postInstantiationOrUpdatingProviderZeroExAsync();
|
||||
delete this._ledgerSubprovider;
|
||||
delete this._cachedProvider;
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
throw utils.spawnSwitchErr('providerType', providerType);
|
||||
const isU2FSupported = await utils.isU2FSupportedAsync();
|
||||
if (!isU2FSupported) {
|
||||
throw new Error('Cannot update providerType to LEDGER without U2F support');
|
||||
}
|
||||
|
||||
await this._fetchTokenInformationAsync();
|
||||
// Cache injected provider so that we can switch the user back to it easily
|
||||
this._cachedProvider = this._web3Wrapper.getProviderObj();
|
||||
this._cachedProviderNetworkId = this.networkId;
|
||||
|
||||
this._userAddress = '';
|
||||
this._dispatcher.updateUserAddress(''); // Clear old userAddress
|
||||
|
||||
const provider = new ProviderEngine();
|
||||
const ledgerWalletConfigs = {
|
||||
networkId,
|
||||
ledgerEthereumClientFactoryAsync: ledgerEthereumBrowserClientFactoryAsync,
|
||||
};
|
||||
this._ledgerSubprovider = new LedgerSubprovider(ledgerWalletConfigs);
|
||||
provider.addProvider(this._ledgerSubprovider);
|
||||
provider.addProvider(new FilterSubprovider());
|
||||
provider.addProvider(new RedundantRPCSubprovider(configs.PUBLIC_NODE_URLS_BY_NETWORK_ID[networkId]));
|
||||
provider.start();
|
||||
this._web3Wrapper.destroy();
|
||||
this.networkId = networkId;
|
||||
this._dispatcher.updateNetworkId(this.networkId);
|
||||
const shouldPollUserAddress = false;
|
||||
this._web3Wrapper = new Web3Wrapper(this._dispatcher, provider, this.networkId, shouldPollUserAddress);
|
||||
this._zeroEx.setProvider(provider, this.networkId);
|
||||
await this._postInstantiationOrUpdatingProviderZeroExAsync();
|
||||
this._dispatcher.updateProviderType(ProviderType.Ledger);
|
||||
}
|
||||
public async updateProviderToInjectedAsync() {
|
||||
utils.assert(!_.isUndefined(this._zeroEx), 'ZeroEx must be instantiated.');
|
||||
|
||||
if (_.isUndefined(this._cachedProvider)) {
|
||||
return; // Going from injected to injected, so we noop
|
||||
}
|
||||
const provider = this._cachedProvider;
|
||||
this.networkId = this._cachedProviderNetworkId;
|
||||
this._dispatcher.updateNetworkId(this.networkId);
|
||||
|
||||
this._web3Wrapper.destroy();
|
||||
const shouldPollUserAddress = true;
|
||||
this._web3Wrapper = new Web3Wrapper(this._dispatcher, provider, this.networkId, shouldPollUserAddress);
|
||||
|
||||
this._userAddress = await this._web3Wrapper.getFirstAccountIfExistsAsync();
|
||||
this._dispatcher.updateUserAddress(this._userAddress);
|
||||
this._zeroEx.setProvider(provider, this.networkId);
|
||||
await this._postInstantiationOrUpdatingProviderZeroExAsync();
|
||||
await this.fetchTokenInformationAsync();
|
||||
this._dispatcher.updateProviderType(ProviderType.Injected);
|
||||
delete this._ledgerSubprovider;
|
||||
delete this._cachedProvider;
|
||||
}
|
||||
public async setProxyAllowanceAsync(token: Token, amountInBaseUnits: BigNumber): Promise<void> {
|
||||
utils.assert(this.isValidAddress(token.address), BlockchainCallErrs.TokenAddressIsInvalid);
|
||||
@@ -452,6 +452,7 @@ export class Blockchain {
|
||||
return [balance, allowance];
|
||||
}
|
||||
public async updateTokenBalancesAndAllowancesAsync(tokens: Token[]) {
|
||||
const err = new Error('show stopper');
|
||||
const tokenStateByAddress: TokenStateByAddress = {};
|
||||
for (const token of tokens) {
|
||||
let balance = new BigNumber(0);
|
||||
@@ -483,6 +484,60 @@ export class Blockchain {
|
||||
this._web3Wrapper.destroy();
|
||||
this._stopWatchingExchangeLogFillEvents();
|
||||
}
|
||||
public async fetchTokenInformationAsync() {
|
||||
utils.assert(
|
||||
!_.isUndefined(this.networkId),
|
||||
'Cannot call fetchTokenInformationAsync if disconnected from Ethereum node',
|
||||
);
|
||||
|
||||
this._dispatcher.updateBlockchainIsLoaded(false);
|
||||
// HACK: Without this timeout, the second call to dispatcher somehow causes blockchainIsLoaded
|
||||
// to flicker... Need to debug further :(((())))
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
this._dispatcher.clearTokenByAddress();
|
||||
|
||||
const tokenRegistryTokensByAddress = await this._getTokenRegistryTokensByAddressAsync();
|
||||
|
||||
// HACK: This is a hack so that the loading spinner doesn't show up twice...
|
||||
// Once for loading the blockchain, another for loading the userAddress
|
||||
this._userAddress = await this._web3Wrapper.getFirstAccountIfExistsAsync();
|
||||
if (!_.isEmpty(this._userAddress)) {
|
||||
this._dispatcher.updateUserAddress(this._userAddress);
|
||||
}
|
||||
|
||||
let trackedTokensIfExists = trackedTokenStorage.getTrackedTokensIfExists(this._userAddress, this.networkId);
|
||||
const tokenRegistryTokens = _.values(tokenRegistryTokensByAddress);
|
||||
if (_.isUndefined(trackedTokensIfExists)) {
|
||||
trackedTokensIfExists = _.map(configs.DEFAULT_TRACKED_TOKEN_SYMBOLS, symbol => {
|
||||
const token = _.find(tokenRegistryTokens, t => t.symbol === symbol);
|
||||
token.isTracked = true;
|
||||
return token;
|
||||
});
|
||||
_.each(trackedTokensIfExists, token => {
|
||||
trackedTokenStorage.addTrackedTokenToUser(this._userAddress, this.networkId, token);
|
||||
});
|
||||
} else {
|
||||
// Properly set all tokenRegistry tokens `isTracked` to true if they are in the existing trackedTokens array
|
||||
_.each(trackedTokensIfExists, trackedToken => {
|
||||
if (!_.isUndefined(tokenRegistryTokensByAddress[trackedToken.address])) {
|
||||
tokenRegistryTokensByAddress[trackedToken.address].isTracked = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
const allTokens = _.uniq([...tokenRegistryTokens, ...trackedTokensIfExists]);
|
||||
this._dispatcher.updateTokenByAddress(allTokens);
|
||||
|
||||
// Get balance/allowance for tracked tokens
|
||||
await this.updateTokenBalancesAndAllowancesAsync(trackedTokensIfExists);
|
||||
|
||||
const mostPopularTradingPairTokens: Token[] = [
|
||||
_.find(allTokens, { symbol: configs.DEFAULT_TRACKED_TOKEN_SYMBOLS[0] }),
|
||||
_.find(allTokens, { symbol: configs.DEFAULT_TRACKED_TOKEN_SYMBOLS[1] }),
|
||||
];
|
||||
this._dispatcher.updateChosenAssetTokenAddress(Side.Deposit, mostPopularTradingPairTokens[0].address);
|
||||
this._dispatcher.updateChosenAssetTokenAddress(Side.Receive, mostPopularTradingPairTokens[1].address);
|
||||
this._dispatcher.updateBlockchainIsLoaded(true);
|
||||
}
|
||||
private async _showEtherScanLinkAndAwaitTransactionMinedAsync(
|
||||
txHash: string,
|
||||
): Promise<TransactionReceiptWithDecodedLogs> {
|
||||
@@ -690,60 +745,6 @@ export class Blockchain {
|
||||
: constants.PROVIDER_NAME_PUBLIC;
|
||||
this._dispatcher.updateInjectedProviderName(providerName);
|
||||
}
|
||||
private async _fetchTokenInformationAsync() {
|
||||
utils.assert(
|
||||
!_.isUndefined(this.networkId),
|
||||
'Cannot call fetchTokenInformationAsync if disconnected from Ethereum node',
|
||||
);
|
||||
|
||||
this._dispatcher.updateBlockchainIsLoaded(false);
|
||||
this._dispatcher.clearTokenByAddress();
|
||||
|
||||
const tokenRegistryTokensByAddress = await this._getTokenRegistryTokensByAddressAsync();
|
||||
|
||||
// HACK: We need to fetch the userAddress here because otherwise we cannot save the
|
||||
// tracked tokens in localStorage under the users address nor fetch the token
|
||||
// balances and allowances and we need to do this in order not to trigger the blockchain
|
||||
// loading dialog to show up twice. First to load the contracts, and second to load the
|
||||
// balances and allowances.
|
||||
this._userAddress = await this._web3Wrapper.getFirstAccountIfExistsAsync();
|
||||
if (!_.isEmpty(this._userAddress)) {
|
||||
this._dispatcher.updateUserAddress(this._userAddress);
|
||||
}
|
||||
|
||||
let trackedTokensIfExists = trackedTokenStorage.getTrackedTokensIfExists(this._userAddress, this.networkId);
|
||||
const tokenRegistryTokens = _.values(tokenRegistryTokensByAddress);
|
||||
if (_.isUndefined(trackedTokensIfExists)) {
|
||||
trackedTokensIfExists = _.map(configs.DEFAULT_TRACKED_TOKEN_SYMBOLS, symbol => {
|
||||
const token = _.find(tokenRegistryTokens, t => t.symbol === symbol);
|
||||
token.isTracked = true;
|
||||
return token;
|
||||
});
|
||||
_.each(trackedTokensIfExists, token => {
|
||||
trackedTokenStorage.addTrackedTokenToUser(this._userAddress, this.networkId, token);
|
||||
});
|
||||
} else {
|
||||
// Properly set all tokenRegistry tokens `isTracked` to true if they are in the existing trackedTokens array
|
||||
_.each(trackedTokensIfExists, trackedToken => {
|
||||
if (!_.isUndefined(tokenRegistryTokensByAddress[trackedToken.address])) {
|
||||
tokenRegistryTokensByAddress[trackedToken.address].isTracked = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
const allTokens = _.uniq([...tokenRegistryTokens, ...trackedTokensIfExists]);
|
||||
this._dispatcher.updateTokenByAddress(allTokens);
|
||||
|
||||
// Get balance/allowance for tracked tokens
|
||||
await this.updateTokenBalancesAndAllowancesAsync(trackedTokensIfExists);
|
||||
|
||||
const mostPopularTradingPairTokens: Token[] = [
|
||||
_.find(allTokens, { symbol: configs.DEFAULT_TRACKED_TOKEN_SYMBOLS[0] }),
|
||||
_.find(allTokens, { symbol: configs.DEFAULT_TRACKED_TOKEN_SYMBOLS[1] }),
|
||||
];
|
||||
this._dispatcher.updateChosenAssetTokenAddress(Side.Deposit, mostPopularTradingPairTokens[0].address);
|
||||
this._dispatcher.updateChosenAssetTokenAddress(Side.Receive, mostPopularTradingPairTokens[1].address);
|
||||
this._dispatcher.updateBlockchainIsLoaded(true);
|
||||
}
|
||||
private async _instantiateContractIfExistsAsync(artifact: any, address?: string): Promise<ContractInstance> {
|
||||
const c = await contract(artifact);
|
||||
const providerObj = this._web3Wrapper.getProviderObj();
|
||||
|
||||
@@ -7,8 +7,10 @@ import TextField from 'material-ui/TextField';
|
||||
import * as React from 'react';
|
||||
import ReactTooltip = require('react-tooltip');
|
||||
import { Blockchain } from 'ts/blockchain';
|
||||
import { NetworkDropDown } from 'ts/components/dropdowns/network_drop_down';
|
||||
import { LifeCycleRaisedButton } from 'ts/components/ui/lifecycle_raised_button';
|
||||
import { Dispatcher } from 'ts/redux/dispatcher';
|
||||
import { ProviderType } from 'ts/types';
|
||||
import { colors } from 'ts/utils/colors';
|
||||
import { configs } from 'ts/utils/configs';
|
||||
import { constants } from 'ts/utils/constants';
|
||||
@@ -27,27 +29,30 @@ interface LedgerConfigDialogProps {
|
||||
dispatcher: Dispatcher;
|
||||
blockchain: Blockchain;
|
||||
networkId: number;
|
||||
providerType: ProviderType;
|
||||
}
|
||||
|
||||
interface LedgerConfigDialogState {
|
||||
didConnectFail: boolean;
|
||||
connectionErrMsg: string;
|
||||
stepIndex: LedgerSteps;
|
||||
userAddresses: string[];
|
||||
addressBalances: BigNumber[];
|
||||
derivationPath: string;
|
||||
derivationErrMsg: string;
|
||||
preferredNetworkId: number;
|
||||
}
|
||||
|
||||
export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps, LedgerConfigDialogState> {
|
||||
constructor(props: LedgerConfigDialogProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
didConnectFail: false,
|
||||
connectionErrMsg: '',
|
||||
stepIndex: LedgerSteps.CONNECT,
|
||||
userAddresses: [],
|
||||
addressBalances: [],
|
||||
derivationPath: configs.DEFAULT_DERIVATION_PATH,
|
||||
derivationErrMsg: '',
|
||||
preferredNetworkId: props.networkId,
|
||||
};
|
||||
}
|
||||
public render() {
|
||||
@@ -77,7 +82,7 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps,
|
||||
return (
|
||||
<div>
|
||||
<div className="h4 pt3">Follow these instructions before proceeding:</div>
|
||||
<ol>
|
||||
<ol className="mb0">
|
||||
<li className="pb1">Connect your Ledger Nano S & Open the Ethereum application</li>
|
||||
<li className="pb1">Verify that Browser Support is enabled in Settings</li>
|
||||
<li className="pb1">
|
||||
@@ -86,7 +91,15 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps,
|
||||
Firmware >1.2
|
||||
</a>
|
||||
</li>
|
||||
<li>Choose your desired network:</li>
|
||||
</ol>
|
||||
<div className="pb2">
|
||||
<NetworkDropDown
|
||||
updateSelectedNetwork={this._onSelectedNetworkUpdated.bind(this)}
|
||||
selectedNetworkId={this.state.preferredNetworkId}
|
||||
avialableNetworkIds={[1, 42]}
|
||||
/>
|
||||
</div>
|
||||
<div className="center pb3">
|
||||
<LifeCycleRaisedButton
|
||||
isPrimary={true}
|
||||
@@ -95,9 +108,9 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps,
|
||||
labelComplete="Connected!"
|
||||
onClickAsyncFn={this._onConnectLedgerClickAsync.bind(this, true)}
|
||||
/>
|
||||
{this.state.didConnectFail && (
|
||||
{!_.isEmpty(this.state.connectionErrMsg) && (
|
||||
<div className="pt2 left-align" style={{ color: colors.red200 }}>
|
||||
Failed to connect. Follow the instructions and try again.
|
||||
{this.state.connectionErrMsg}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -172,7 +185,7 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps,
|
||||
}
|
||||
private _onClose() {
|
||||
this.setState({
|
||||
didConnectFail: false,
|
||||
connectionErrMsg: '',
|
||||
});
|
||||
const isOpen = false;
|
||||
this.props.toggleDialogFn(isOpen);
|
||||
@@ -184,6 +197,7 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps,
|
||||
const selectAddressBalance = this.state.addressBalances[selectedRowIndex];
|
||||
this.props.dispatcher.updateUserAddress(selectedAddress);
|
||||
this.props.blockchain.updateWeb3WrapperPrevUserAddress(selectedAddress);
|
||||
this.props.blockchain.fetchTokenInformationAsync(); // fire and forget
|
||||
this.props.dispatcher.updateUserEtherBalance(selectAddressBalance);
|
||||
this.setState({
|
||||
stepIndex: LedgerSteps.CONNECT,
|
||||
@@ -219,7 +233,7 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps,
|
||||
} catch (err) {
|
||||
utils.consoleLog(`Ledger error: ${JSON.stringify(err)}`);
|
||||
this.setState({
|
||||
didConnectFail: true,
|
||||
connectionErrMsg: 'Failed to connect. Follow the instructions and try again.',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
@@ -241,6 +255,19 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps,
|
||||
});
|
||||
}
|
||||
private async _onConnectLedgerClickAsync() {
|
||||
const isU2FSupported = await utils.isU2FSupportedAsync();
|
||||
if (!isU2FSupported) {
|
||||
utils.consoleLog(`U2F not supported in this browser`);
|
||||
this.setState({
|
||||
connectionErrMsg: 'U2F not supported by this browser. Try using Chrome.',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.props.providerType !== ProviderType.Ledger) {
|
||||
await this.props.blockchain.updateProviderToLedgerAsync(this.state.preferredNetworkId);
|
||||
}
|
||||
|
||||
const didSucceed = await this._fetchAddressesAndBalancesAsync();
|
||||
if (didSucceed) {
|
||||
this.setState({
|
||||
@@ -258,4 +285,9 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps,
|
||||
}
|
||||
return userAddresses;
|
||||
}
|
||||
private _onSelectedNetworkUpdated(e: any, index: number, networkId: number) {
|
||||
this.setState({
|
||||
preferredNetworkId: networkId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import * as _ from 'lodash';
|
||||
import DropDownMenu from 'material-ui/DropDownMenu';
|
||||
import MenuItem from 'material-ui/MenuItem';
|
||||
import * as React from 'react';
|
||||
import { constants } from 'ts/utils/constants';
|
||||
|
||||
interface NetworkDropDownProps {
|
||||
updateSelectedNetwork: (e: any, index: number, value: number) => void;
|
||||
selectedNetworkId: number;
|
||||
avialableNetworkIds: number[];
|
||||
}
|
||||
|
||||
interface NetworkDropDownState {}
|
||||
|
||||
export class NetworkDropDown extends React.Component<NetworkDropDownProps, NetworkDropDownState> {
|
||||
public render() {
|
||||
return (
|
||||
<div className="mx-auto" style={{ width: 120 }}>
|
||||
<DropDownMenu value={this.props.selectedNetworkId} onChange={this.props.updateSelectedNetwork}>
|
||||
{this._renderDropDownItems()}
|
||||
</DropDownMenu>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private _renderDropDownItems() {
|
||||
const items = _.map(this.props.avialableNetworkIds, networkId => {
|
||||
const networkName = constants.NETWORK_NAME_BY_ID[networkId];
|
||||
const primaryText = (
|
||||
<div className="flex">
|
||||
<div className="pr1" style={{ width: 14, paddingTop: 2 }}>
|
||||
<img src={`/images/network_icons/${networkName.toLowerCase()}.png`} style={{ width: 14 }} />
|
||||
</div>
|
||||
<div>{networkName}</div>
|
||||
</div>
|
||||
);
|
||||
return <MenuItem key={networkId} value={networkId} primaryText={primaryText} />;
|
||||
});
|
||||
return items;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ 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';
|
||||
@@ -13,19 +14,21 @@ 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 { 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,
|
||||
@@ -46,9 +49,11 @@ export interface PortalAllProps {
|
||||
blockchainIsLoaded: boolean;
|
||||
dispatcher: Dispatcher;
|
||||
hashData: HashData;
|
||||
injectedProviderName: string;
|
||||
networkId: number;
|
||||
nodeVersion: string;
|
||||
orderFillAmount: BigNumber;
|
||||
providerType: ProviderType;
|
||||
screenWidth: ScreenWidths;
|
||||
tokenByAddress: TokenByAddress;
|
||||
tokenStateByAddress: TokenStateByAddress;
|
||||
@@ -67,6 +72,7 @@ interface PortalAllState {
|
||||
prevPathname: string;
|
||||
isDisclaimerDialogOpen: boolean;
|
||||
isWethNoticeDialogOpen: boolean;
|
||||
isLedgerDialogOpen: boolean;
|
||||
}
|
||||
|
||||
export class Portal extends React.Component<PortalAllProps, PortalAllState> {
|
||||
@@ -96,6 +102,7 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> {
|
||||
prevPathname: this.props.location.pathname,
|
||||
isDisclaimerDialogOpen: !hasAcceptedDisclaimer,
|
||||
isWethNoticeDialogOpen: !hasAlreadyDismissedWethNotice && isViewingBalances,
|
||||
isLedgerDialogOpen: false,
|
||||
};
|
||||
}
|
||||
public componentDidMount() {
|
||||
@@ -127,8 +134,9 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> {
|
||||
this._blockchain.userAddressUpdatedFireAndForgetAsync(nextProps.userAddress);
|
||||
if (!_.isEmpty(nextProps.userAddress) && nextProps.blockchainIsLoaded) {
|
||||
const tokens = _.values(nextProps.tokenByAddress);
|
||||
const trackedTokens = _.filter(tokens, t => t.isTracked);
|
||||
// tslint:disable-next-line:no-floating-promises
|
||||
this._updateBalanceAndAllowanceWithLoadingScreenAsync(tokens);
|
||||
this._updateBalanceAndAllowanceWithLoadingScreenAsync(trackedTokens);
|
||||
}
|
||||
this.setState({
|
||||
prevUserAddress: nextProps.userAddress,
|
||||
@@ -167,8 +175,14 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> {
|
||||
<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}
|
||||
/>
|
||||
<div id="portal" className="mx-auto max-width-4" style={{ width: '100%' }}>
|
||||
<Paper className="mb3 mt2">
|
||||
@@ -239,11 +253,26 @@ export class Portal extends React.Component<PortalAllProps, PortalAllState> {
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Footer />
|
||||
<Footer />;
|
||||
</div>
|
||||
);
|
||||
}
|
||||
public onToggleLedgerDialog() {
|
||||
this.setState({
|
||||
isLedgerDialogOpen: !this.state.isLedgerDialogOpen,
|
||||
});
|
||||
}
|
||||
private _renderEthWrapper() {
|
||||
return (
|
||||
<EthWrappers
|
||||
|
||||
149
packages/website/ts/components/top_bar/provider_display.tsx
Normal file
149
packages/website/ts/components/top_bar/provider_display.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import * as _ from 'lodash';
|
||||
import Menu from 'material-ui/Menu';
|
||||
import MenuItem from 'material-ui/MenuItem';
|
||||
import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton';
|
||||
import RaisedButton from 'material-ui/RaisedButton';
|
||||
import * as React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Blockchain } from 'ts/blockchain';
|
||||
import { ProviderPicker } from 'ts/components/top_bar/provider_picker';
|
||||
import { DropDown } from 'ts/components/ui/drop_down';
|
||||
import { Identicon } from 'ts/components/ui/identicon';
|
||||
import { Dispatcher } from 'ts/redux/dispatcher';
|
||||
import { ProviderType } from 'ts/types';
|
||||
import { colors } from 'ts/utils/colors';
|
||||
import { constants } from 'ts/utils/constants';
|
||||
import { utils } from 'ts/utils/utils';
|
||||
|
||||
const IDENTICON_DIAMETER = 32;
|
||||
|
||||
interface ProviderDisplayProps {
|
||||
dispatcher: Dispatcher;
|
||||
userAddress: string;
|
||||
networkId: number;
|
||||
injectedProviderName: string;
|
||||
providerType: ProviderType;
|
||||
onToggleLedgerDialog: () => void;
|
||||
blockchain: Blockchain;
|
||||
}
|
||||
|
||||
interface ProviderDisplayState {}
|
||||
|
||||
export class ProviderDisplay extends React.Component<ProviderDisplayProps, ProviderDisplayState> {
|
||||
public render() {
|
||||
const isAddressAvailable = !_.isEmpty(this.props.userAddress);
|
||||
const isExternallyInjectedProvider = ProviderType.Injected && this.props.injectedProviderName !== '0x Public';
|
||||
const displayAddress = isAddressAvailable
|
||||
? utils.getAddressBeginAndEnd(this.props.userAddress)
|
||||
: isExternallyInjectedProvider ? 'Account locked' : '0x0000...0000';
|
||||
// If the "injected" provider is our fallback public node, then we want to
|
||||
// show the "connect a wallet" message instead of the providerName
|
||||
const injectedProviderName = isExternallyInjectedProvider
|
||||
? this.props.injectedProviderName
|
||||
: 'Connect a wallet';
|
||||
const providerTitle =
|
||||
this.props.providerType === ProviderType.Injected ? injectedProviderName : 'Ledger Nano S';
|
||||
const hoverActiveNode = (
|
||||
<div className="flex right lg-pr0 md-pr2 sm-pr2" style={{ paddingTop: 16 }}>
|
||||
<div>
|
||||
<Identicon address={this.props.userAddress} diameter={IDENTICON_DIAMETER} />
|
||||
</div>
|
||||
<div style={{ marginLeft: 12, paddingTop: 1 }}>
|
||||
<div style={{ fontSize: 12, color: '#FF7F00' }}>{providerTitle}</div>
|
||||
<div style={{ fontSize: 14 }}>{displayAddress}</div>
|
||||
</div>
|
||||
<div style={{ borderLeft: '1px solid #E0E0E0', marginLeft: 17, paddingTop: 1 }} className="px2">
|
||||
<i style={{ fontSize: 30, color: '#E0E0E0' }} className="zmdi zmdi zmdi-chevron-down" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const hasInjectedProvider =
|
||||
this.props.injectedProviderName !== '0x Public' && this.props.providerType === ProviderType.Injected;
|
||||
const hasLedgerProvider = this.props.providerType === ProviderType.Ledger;
|
||||
const horizontalPosition = hasInjectedProvider || hasLedgerProvider ? 'left' : 'middle';
|
||||
return (
|
||||
<div style={{ width: 'fit-content', height: 48, float: 'right' }}>
|
||||
<DropDown
|
||||
hoverActiveNode={hoverActiveNode}
|
||||
popoverContent={this.renderPopoverContent(hasInjectedProvider, hasLedgerProvider)}
|
||||
anchorOrigin={{ horizontal: horizontalPosition, vertical: 'bottom' }}
|
||||
targetOrigin={{ horizontal: horizontalPosition, vertical: 'top' }}
|
||||
zDepth={1}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
public renderPopoverContent(hasInjectedProvider: boolean, hasLedgerProvider: boolean) {
|
||||
if (hasInjectedProvider || hasLedgerProvider) {
|
||||
return (
|
||||
<ProviderPicker
|
||||
dispatcher={this.props.dispatcher}
|
||||
networkId={this.props.networkId}
|
||||
injectedProviderName={this.props.injectedProviderName}
|
||||
providerType={this.props.providerType}
|
||||
onToggleLedgerDialog={this.props.onToggleLedgerDialog}
|
||||
blockchain={this.props.blockchain}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
// Nothing to connect to, show install/info popover
|
||||
return (
|
||||
<div className="px2" style={{ maxWidth: 420 }}>
|
||||
<div className="center h4 py2" style={{ color: colors.grey700 }}>
|
||||
Choose a wallet:
|
||||
</div>
|
||||
<div className="flex pb3">
|
||||
<div className="center px2">
|
||||
<div style={{ color: colors.darkGrey }}>Install a browser wallet</div>
|
||||
<div className="py2">
|
||||
<img src="/images/metamask_or_parity.png" width="135" />
|
||||
</div>
|
||||
<div>
|
||||
Use{' '}
|
||||
<a
|
||||
href={constants.URL_METAMASK_CHROME_STORE}
|
||||
target="_blank"
|
||||
style={{ color: colors.lightBlueA700 }}
|
||||
>
|
||||
Metamask
|
||||
</a>{' '}
|
||||
or{' '}
|
||||
<a
|
||||
href={constants.URL_PARITY_CHROME_STORE}
|
||||
target="_blank"
|
||||
style={{ color: colors.lightBlueA700 }}
|
||||
>
|
||||
Parity Signer
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className="pl1 ml1"
|
||||
style={{ borderLeft: `1px solid ${colors.grey300}`, height: 65 }}
|
||||
/>
|
||||
<div className="py1">or</div>
|
||||
<div
|
||||
className="pl1 ml1"
|
||||
style={{ borderLeft: `1px solid ${colors.grey300}`, height: 68 }}
|
||||
/>
|
||||
</div>
|
||||
<div className="px2 center">
|
||||
<div style={{ color: colors.darkGrey }}>Connect to a ledger hardware wallet</div>
|
||||
<div style={{ paddingTop: 21, paddingBottom: 29 }}>
|
||||
<img src="/images/ledger_icon.png" style={{ width: 80 }} />
|
||||
</div>
|
||||
<div>
|
||||
<RaisedButton
|
||||
style={{ width: '100%' }}
|
||||
label="Use Ledger"
|
||||
onClick={this.props.onToggleLedgerDialog}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
88
packages/website/ts/components/top_bar/provider_picker.tsx
Normal file
88
packages/website/ts/components/top_bar/provider_picker.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import * as _ from 'lodash';
|
||||
import Menu from 'material-ui/Menu';
|
||||
import MenuItem from 'material-ui/MenuItem';
|
||||
import { RadioButton, RadioButtonGroup } from 'material-ui/RadioButton';
|
||||
import * as React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Blockchain } from 'ts/blockchain';
|
||||
import { DropDown } from 'ts/components/ui/drop_down';
|
||||
import { Identicon } from 'ts/components/ui/identicon';
|
||||
import { Dispatcher } from 'ts/redux/dispatcher';
|
||||
import { ProviderType } from 'ts/types';
|
||||
import { constants } from 'ts/utils/constants';
|
||||
import { utils } from 'ts/utils/utils';
|
||||
|
||||
const IDENTICON_DIAMETER = 32;
|
||||
const SELECTED_BG_COLOR = '#F7F7F7';
|
||||
|
||||
interface ProviderPickerProps {
|
||||
networkId: number;
|
||||
injectedProviderName: string;
|
||||
providerType: ProviderType;
|
||||
onToggleLedgerDialog: () => void;
|
||||
dispatcher: Dispatcher;
|
||||
blockchain: Blockchain;
|
||||
}
|
||||
|
||||
interface ProviderPickerState {}
|
||||
|
||||
export class ProviderPicker extends React.Component<ProviderPickerProps, ProviderPickerState> {
|
||||
public render() {
|
||||
const isLedgerSelected = this.props.providerType === ProviderType.Ledger;
|
||||
const menuStyle = {
|
||||
padding: 10,
|
||||
paddingTop: 15,
|
||||
paddingBottom: 15,
|
||||
};
|
||||
const injectedLabel = (
|
||||
<div className="flex">
|
||||
<div>{this.props.injectedProviderName}</div>
|
||||
{this._renderNetwork()}
|
||||
</div>
|
||||
);
|
||||
// Show dropdown with two options
|
||||
return (
|
||||
<div style={{ width: 225, overflow: 'hidden' }}>
|
||||
<RadioButtonGroup
|
||||
name="provider"
|
||||
defaultSelected={this.props.providerType}
|
||||
onChange={this._onProviderRadioChanged.bind(this)}
|
||||
>
|
||||
<RadioButton
|
||||
style={{ ...menuStyle, backgroundColor: !isLedgerSelected && SELECTED_BG_COLOR }}
|
||||
value={ProviderType.Injected}
|
||||
label={injectedLabel}
|
||||
/>
|
||||
<RadioButton
|
||||
style={{ ...menuStyle, backgroundColor: isLedgerSelected && SELECTED_BG_COLOR }}
|
||||
value={ProviderType.Ledger}
|
||||
label="Ledger Nano S"
|
||||
/>
|
||||
</RadioButtonGroup>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private _renderNetwork() {
|
||||
const networkName = constants.NETWORK_NAME_BY_ID[this.props.networkId];
|
||||
return (
|
||||
<div className="flex">
|
||||
<div className="pr1 relative" style={{ width: 14, paddingLeft: 14 }}>
|
||||
<img
|
||||
src={`/images/network_icons/${networkName.toLowerCase()}.png`}
|
||||
className="absolute"
|
||||
style={{ top: 4, width: 14 }}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ color: '#BBBBBB' }}>{networkName}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private _onProviderRadioChanged(e: any, value: string) {
|
||||
if (value === ProviderType.Ledger) {
|
||||
this.props.onToggleLedgerDialog();
|
||||
} else {
|
||||
// Fire and forget
|
||||
this.props.blockchain.updateProviderToInjectedAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,32 @@
|
||||
import * as _ from 'lodash';
|
||||
import Drawer from 'material-ui/Drawer';
|
||||
import Menu from 'material-ui/Menu';
|
||||
import MenuItem from 'material-ui/MenuItem';
|
||||
import * as React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import ReactTooltip = require('react-tooltip');
|
||||
import { Blockchain } from 'ts/blockchain';
|
||||
import { PortalMenu } from 'ts/components/portal_menu';
|
||||
import { TopBarMenuItem } from 'ts/components/top_bar_menu_item';
|
||||
import { DropDownMenuItem } from 'ts/components/ui/drop_down_menu_item';
|
||||
import { ProviderDisplay } from 'ts/components/top_bar/provider_display';
|
||||
import { TopBarMenuItem } from 'ts/components/top_bar/top_bar_menu_item';
|
||||
import { DropDown } from 'ts/components/ui/drop_down';
|
||||
import { Identicon } from 'ts/components/ui/identicon';
|
||||
import { DocsInfo } from 'ts/pages/documentation/docs_info';
|
||||
import { NestedSidebarMenu } from 'ts/pages/shared/nested_sidebar_menu';
|
||||
import { DocsMenu, MenuSubsectionsBySection, Styles, WebsitePaths } from 'ts/types';
|
||||
import { Dispatcher } from 'ts/redux/dispatcher';
|
||||
import { DocsMenu, MenuSubsectionsBySection, ProviderType, Styles, TypeDocNode, WebsitePaths } from 'ts/types';
|
||||
import { colors } from 'ts/utils/colors';
|
||||
import { configs } from 'ts/utils/configs';
|
||||
import { constants } from 'ts/utils/constants';
|
||||
|
||||
interface TopBarProps {
|
||||
userAddress?: string;
|
||||
networkId?: number;
|
||||
injectedProviderName?: string;
|
||||
providerType?: ProviderType;
|
||||
onToggleLedgerDialog?: () => void;
|
||||
blockchain?: Blockchain;
|
||||
dispatcher?: Dispatcher;
|
||||
blockchainIsLoaded: boolean;
|
||||
location: Location;
|
||||
docsVersion?: string;
|
||||
@@ -125,6 +136,15 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> {
|
||||
cursor: 'pointer',
|
||||
paddingTop: 16,
|
||||
};
|
||||
const hoverActiveNode = (
|
||||
<div className="flex relative" style={{ color: menuIconStyle.color }}>
|
||||
<div style={{ paddingRight: 10 }}>Developers</div>
|
||||
<div className="absolute" style={{ paddingLeft: 3, right: 3, top: -2 }}>
|
||||
<i className="zmdi zmdi-caret-right" style={{ fontSize: 22 }} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
const popoverContent = <Menu style={{ color: colors.darkGrey }}>{developerSectionMenuItems}</Menu>;
|
||||
return (
|
||||
<div style={{ ...styles.topBar, ...bottomBorderStyle, ...this.props.style }} className="pb1">
|
||||
<div className={parentClassNames}>
|
||||
@@ -138,11 +158,12 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> {
|
||||
{!this._isViewingPortal() && (
|
||||
<div className={menuClasses}>
|
||||
<div className="flex justify-between">
|
||||
<DropDownMenuItem
|
||||
title="Developers"
|
||||
subMenuItems={developerSectionMenuItems}
|
||||
<DropDown
|
||||
hoverActiveNode={hoverActiveNode}
|
||||
popoverContent={popoverContent}
|
||||
anchorOrigin={{ horizontal: 'middle', vertical: 'bottom' }}
|
||||
targetOrigin={{ horizontal: 'middle', vertical: 'top' }}
|
||||
style={styles.menuItem}
|
||||
isNightVersion={isNightVersion}
|
||||
/>
|
||||
<TopBarMenuItem
|
||||
title="Wiki"
|
||||
@@ -167,10 +188,19 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{this.props.blockchainIsLoaded &&
|
||||
!_.isEmpty(this.props.userAddress) && (
|
||||
<div className="col col-5 sm-hide xs-hide">{this._renderUser()}</div>
|
||||
)}
|
||||
{this.props.blockchainIsLoaded && (
|
||||
<div className="col col-5">
|
||||
<ProviderDisplay
|
||||
dispatcher={this.props.dispatcher}
|
||||
userAddress={this.props.userAddress}
|
||||
networkId={this.props.networkId}
|
||||
injectedProviderName={this.props.injectedProviderName}
|
||||
providerType={this.props.providerType}
|
||||
onToggleLedgerDialog={this.props.onToggleLedgerDialog}
|
||||
blockchain={this.props.blockchain}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={`col ${isFullWidthPage ? 'col-2 pl2' : 'col-1'} md-hide lg-hide`}>
|
||||
<div style={menuIconStyle}>
|
||||
<i className="zmdi zmdi-menu" onClick={this._onMenuButtonClick.bind(this)} />
|
||||
@@ -1,7 +1,8 @@
|
||||
import * as _ from 'lodash';
|
||||
import Menu from 'material-ui/Menu';
|
||||
import Popover from 'material-ui/Popover';
|
||||
import Popover, { PopoverAnimationVertical } from 'material-ui/Popover';
|
||||
import * as React from 'react';
|
||||
import { MaterialUIPosition, Styles, WebsitePaths } from 'ts/types';
|
||||
import { colors } from 'ts/utils/colors';
|
||||
|
||||
const CHECK_CLOSE_POPOVER_INTERVAL_MS = 300;
|
||||
@@ -9,28 +10,28 @@ const DEFAULT_STYLE = {
|
||||
fontSize: 14,
|
||||
};
|
||||
|
||||
interface DropDownMenuItemProps {
|
||||
title: string;
|
||||
subMenuItems: React.ReactNode[];
|
||||
interface DropDownProps {
|
||||
hoverActiveNode: React.ReactNode;
|
||||
popoverContent: React.ReactNode;
|
||||
anchorOrigin: MaterialUIPosition;
|
||||
targetOrigin: MaterialUIPosition;
|
||||
style?: React.CSSProperties;
|
||||
menuItemStyle?: React.CSSProperties;
|
||||
isNightVersion?: boolean;
|
||||
zDepth?: number;
|
||||
}
|
||||
|
||||
interface DropDownMenuItemState {
|
||||
interface DropDownState {
|
||||
isDropDownOpen: boolean;
|
||||
anchorEl?: HTMLInputElement;
|
||||
}
|
||||
|
||||
export class DropDownMenuItem extends React.Component<DropDownMenuItemProps, DropDownMenuItemState> {
|
||||
public static defaultProps: Partial<DropDownMenuItemProps> = {
|
||||
export class DropDown extends React.Component<DropDownProps, DropDownState> {
|
||||
public static defaultProps: Partial<DropDownProps> = {
|
||||
style: DEFAULT_STYLE,
|
||||
menuItemStyle: DEFAULT_STYLE,
|
||||
isNightVersion: false,
|
||||
zDepth: 1,
|
||||
};
|
||||
private _isHovering: boolean;
|
||||
private _popoverCloseCheckIntervalId: number;
|
||||
constructor(props: DropDownMenuItemProps) {
|
||||
constructor(props: DropDownProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isDropDownOpen: false,
|
||||
@@ -44,30 +45,35 @@ export class DropDownMenuItem extends React.Component<DropDownMenuItemProps, Dro
|
||||
public componentWillUnmount() {
|
||||
window.clearInterval(this._popoverCloseCheckIntervalId);
|
||||
}
|
||||
public componentWillReceiveProps(nextProps: DropDownProps) {
|
||||
// HACK: If the popoverContent is updated to a different dimension and the users
|
||||
// mouse is no longer above it, the dropdown can enter an inconsistent state where
|
||||
// it believes the user is still hovering over it. In order to remedy this, we
|
||||
// call hoverOff whenever the dropdown receives updated props. This is a hack
|
||||
// because it will effectively close the dropdown on any prop update, barring
|
||||
// dropdowns from having dynamic content.
|
||||
this._onHoverOff();
|
||||
}
|
||||
public render() {
|
||||
const colorStyle = this.props.isNightVersion ? 'white' : this.props.style.color;
|
||||
return (
|
||||
<div
|
||||
style={{ ...this.props.style, color: colorStyle }}
|
||||
style={{ ...this.props.style, width: 'fit-content', height: '100%' }}
|
||||
onMouseEnter={this._onHover.bind(this)}
|
||||
onMouseLeave={this._onHoverOff.bind(this)}
|
||||
>
|
||||
<div className="flex relative">
|
||||
<div style={{ paddingRight: 10 }}>{this.props.title}</div>
|
||||
<div className="absolute" style={{ paddingLeft: 3, right: 3, top: -2 }}>
|
||||
<i className="zmdi zmdi-caret-right" style={{ fontSize: 22 }} />
|
||||
</div>
|
||||
</div>
|
||||
{this.props.hoverActiveNode}
|
||||
<Popover
|
||||
open={this.state.isDropDownOpen}
|
||||
anchorEl={this.state.anchorEl}
|
||||
anchorOrigin={{ horizontal: 'middle', vertical: 'bottom' }}
|
||||
targetOrigin={{ horizontal: 'middle', vertical: 'top' }}
|
||||
anchorOrigin={this.props.anchorOrigin}
|
||||
targetOrigin={this.props.targetOrigin}
|
||||
onRequestClose={this._closePopover.bind(this)}
|
||||
useLayerForClickAway={false}
|
||||
animation={PopoverAnimationVertical}
|
||||
zDepth={this.props.zDepth}
|
||||
>
|
||||
<div onMouseEnter={this._onHover.bind(this)} onMouseLeave={this._onHoverOff.bind(this)}>
|
||||
<Menu style={{ color: colors.grey }}>{this.props.subMenuItems}</Menu>
|
||||
{this.props.popoverContent}
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
@@ -87,7 +93,7 @@ export class DropDownMenuItem extends React.Component<DropDownMenuItemProps, Dro
|
||||
anchorEl: event.currentTarget,
|
||||
});
|
||||
}
|
||||
private _onHoverOff(event: React.FormEvent<HTMLInputElement>) {
|
||||
private _onHoverOff() {
|
||||
this._isHovering = false;
|
||||
}
|
||||
private _checkIfShouldClosePopover() {
|
||||
@@ -6,16 +6,27 @@ import { Dispatch } from 'redux';
|
||||
import { Portal as PortalComponent, PortalAllProps as PortalComponentAllProps } from 'ts/components/portal';
|
||||
import { Dispatcher } from 'ts/redux/dispatcher';
|
||||
import { State } from 'ts/redux/reducer';
|
||||
import { BlockchainErrs, HashData, Order, ScreenWidths, Side, TokenByAddress, TokenStateByAddress } from 'ts/types';
|
||||
import {
|
||||
BlockchainErrs,
|
||||
HashData,
|
||||
Order,
|
||||
ProviderType,
|
||||
ScreenWidths,
|
||||
Side,
|
||||
TokenByAddress,
|
||||
TokenStateByAddress,
|
||||
} from 'ts/types';
|
||||
import { constants } from 'ts/utils/constants';
|
||||
|
||||
interface ConnectedState {
|
||||
blockchainErr: BlockchainErrs;
|
||||
blockchainIsLoaded: boolean;
|
||||
hashData: HashData;
|
||||
injectedProviderName: string;
|
||||
networkId: number;
|
||||
nodeVersion: string;
|
||||
orderFillAmount: BigNumber;
|
||||
providerType: ProviderType;
|
||||
tokenByAddress: TokenByAddress;
|
||||
tokenStateByAddress: TokenStateByAddress;
|
||||
userEtherBalance: BigNumber;
|
||||
@@ -57,10 +68,12 @@ const mapStateToProps = (state: State, ownProps: PortalComponentAllProps): Conne
|
||||
return {
|
||||
blockchainErr: state.blockchainErr,
|
||||
blockchainIsLoaded: state.blockchainIsLoaded,
|
||||
hashData,
|
||||
injectedProviderName: state.injectedProviderName,
|
||||
networkId: state.networkId,
|
||||
nodeVersion: state.nodeVersion,
|
||||
orderFillAmount: state.orderFillAmount,
|
||||
hashData,
|
||||
providerType: state.providerType,
|
||||
screenWidth: state.screenWidth,
|
||||
shouldBlockchainErrDialogBeOpen: state.shouldBlockchainErrDialogBeOpen,
|
||||
tokenByAddress: state.tokenByAddress,
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import * as DocumentTitle from 'react-document-title';
|
||||
import { Footer } from 'ts/components/footer';
|
||||
import { TopBar } from 'ts/components/top_bar';
|
||||
import { TopBar } from 'ts/components/top_bar/top_bar';
|
||||
import { Profile } from 'ts/pages/about/profile';
|
||||
import { ProfileInfo, Styles } from 'ts/types';
|
||||
import { colors } from 'ts/utils/colors';
|
||||
|
||||
@@ -5,7 +5,7 @@ import * as React from 'react';
|
||||
import DocumentTitle = require('react-document-title');
|
||||
import { scroller } from 'react-scroll';
|
||||
import semverSort = require('semver-sort');
|
||||
import { TopBar } from 'ts/components/top_bar';
|
||||
import { TopBar } from 'ts/components/top_bar/top_bar';
|
||||
import { Badge } from 'ts/components/ui/badge';
|
||||
import { Comment } from 'ts/pages/documentation/comment';
|
||||
import { DocsInfo } from 'ts/pages/documentation/docs_info';
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import * as DocumentTitle from 'react-document-title';
|
||||
import { Footer } from 'ts/components/footer';
|
||||
import { TopBar } from 'ts/components/top_bar';
|
||||
import { TopBar } from 'ts/components/top_bar/top_bar';
|
||||
import { Question } from 'ts/pages/faq/question';
|
||||
import { FAQQuestion, FAQSection, Styles, WebsitePaths } from 'ts/types';
|
||||
import { colors } from 'ts/utils/colors';
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as React from 'react';
|
||||
import DocumentTitle = require('react-document-title');
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Footer } from 'ts/components/footer';
|
||||
import { TopBar } from 'ts/components/top_bar';
|
||||
import { TopBar } from 'ts/components/top_bar/top_bar';
|
||||
import { ScreenWidths, WebsitePaths } from 'ts/types';
|
||||
import { colors } from 'ts/utils/colors';
|
||||
import { constants } from 'ts/utils/constants';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import { Footer } from 'ts/components/footer';
|
||||
import { TopBar } from 'ts/components/top_bar';
|
||||
import { TopBar } from 'ts/components/top_bar/top_bar';
|
||||
import { Styles } from 'ts/types';
|
||||
|
||||
export interface NotFoundProps {
|
||||
|
||||
@@ -3,7 +3,7 @@ import CircularProgress from 'material-ui/CircularProgress';
|
||||
import * as React from 'react';
|
||||
import DocumentTitle = require('react-document-title');
|
||||
import { scroller } from 'react-scroll';
|
||||
import { TopBar } from 'ts/components/top_bar';
|
||||
import { TopBar } from 'ts/components/top_bar/top_bar';
|
||||
import { MarkdownSection } from 'ts/pages/shared/markdown_section';
|
||||
import { NestedSidebarMenu } from 'ts/pages/shared/nested_sidebar_menu';
|
||||
import { SectionHeader } from 'ts/pages/shared/section_header';
|
||||
|
||||
@@ -678,4 +678,9 @@ export enum SmartContractDocSections {
|
||||
ZRXToken = 'ZRXToken',
|
||||
}
|
||||
|
||||
export interface MaterialUIPosition {
|
||||
vertical: 'bottom' | 'top' | 'center';
|
||||
horizontal: 'left' | 'middle' | 'right';
|
||||
}
|
||||
|
||||
// tslint:disable:max-file-line-count
|
||||
|
||||
Reference in New Issue
Block a user