diff options
Diffstat (limited to 'packages/website/ts')
54 files changed, 1162 insertions, 873 deletions
diff --git a/packages/website/ts/blockchain.ts b/packages/website/ts/blockchain.ts index 46a4d6629..d18c34c32 100644 --- a/packages/website/ts/blockchain.ts +++ b/packages/website/ts/blockchain.ts @@ -30,6 +30,7 @@ import { import { BigNumber, intervalUtils, logUtils, promisify } from '@0xproject/utils'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; import * as _ from 'lodash'; +import * as moment from 'moment'; import * as React from 'react'; import contract = require('truffle-contract'); import { BlockchainWatcher } from 'ts/blockchain_watcher'; @@ -43,6 +44,8 @@ import { BlockchainErrs, ContractInstance, Fill, + InjectedProviderObservable, + InjectedProviderUpdate, Order as PortalOrder, Providers, ProviderType, @@ -79,9 +82,9 @@ export class Blockchain { private _dispatcher: Dispatcher; private _web3Wrapper?: Web3Wrapper; private _blockchainWatcher?: BlockchainWatcher; + private _injectedProviderObservable?: InjectedProviderObservable; + private _injectedProviderUpdateHandler: (update: InjectedProviderUpdate) => Promise<void>; private _userAddressIfExists: string; - private _cachedProvider: Provider; - private _cachedProviderNetworkId: number; private _ledgerSubprovider: LedgerSubprovider; private _defaultGasPrice: BigNumber; private static _getNameGivenProvider(provider: Provider): string { @@ -92,16 +95,62 @@ export class Blockchain { } return providerNameIfExists; } - private static async _getProviderAsync(injectedWeb3: Web3, networkIdIfExists: number): Promise<Provider> { + private static _getInjectedWeb3(): any { + return (window as any).web3; + } + private static async _getInjectedWeb3ProviderNetworkIdIfExistsAsync(): Promise<number | undefined> { + // Hack: We need to know the networkId the injectedWeb3 is connected to (if it is defined) in + // order to properly instantiate the web3Wrapper. Since we must use the async call, we cannot + // retrieve it from within the web3Wrapper constructor. This is and should remain the only + // call to a web3 instance outside of web3Wrapper in the entire dapp. + // In addition, if the user has an injectedWeb3 instance that is disconnected from a backing + // Ethereum node, this call will throw. We need to handle this case gracefully + const injectedWeb3IfExists = Blockchain._getInjectedWeb3(); + let networkIdIfExists: number; + if (!_.isUndefined(injectedWeb3IfExists)) { + try { + networkIdIfExists = _.parseInt(await promisify<string>(injectedWeb3IfExists.version.getNetwork)()); + } catch (err) { + // Ignore error and proceed with networkId undefined + } + } + return networkIdIfExists; + } + private static async _getProviderAsync( + injectedWeb3: Web3, + networkIdIfExists: number, + shouldUserLedgerProvider: boolean = false, + ): Promise<[Provider, LedgerSubprovider | undefined]> { const doesInjectedWeb3Exist = !_.isUndefined(injectedWeb3); + const isNetworkIdAvailable = !_.isUndefined(networkIdIfExists); const publicNodeUrlsIfExistsForNetworkId = configs.PUBLIC_NODE_URLS_BY_NETWORK_ID[networkIdIfExists]; const isPublicNodeAvailableForNetworkId = !_.isUndefined(publicNodeUrlsIfExistsForNetworkId); - let provider; - if (doesInjectedWeb3Exist && isPublicNodeAvailableForNetworkId) { + if (shouldUserLedgerProvider && isNetworkIdAvailable) { + const isU2FSupported = await utils.isU2FSupportedAsync(); + if (!isU2FSupported) { + throw new Error('Cannot update providerType to LEDGER without U2F support'); + } + const provider = new ProviderEngine(); + const ledgerWalletConfigs = { + networkId: networkIdIfExists, + ledgerEthereumClientFactoryAsync: ledgerEthereumBrowserClientFactoryAsync, + }; + const ledgerSubprovider = new LedgerSubprovider(ledgerWalletConfigs); + provider.addProvider(ledgerSubprovider); + provider.addProvider(new FilterSubprovider()); + const rpcSubproviders = _.map(configs.PUBLIC_NODE_URLS_BY_NETWORK_ID[networkIdIfExists], publicNodeUrl => { + return new RpcSubprovider({ + rpcUrl: publicNodeUrl, + }); + }); + provider.addProvider(new RedundantSubprovider(rpcSubproviders as Subprovider[])); + provider.start(); + return [provider, ledgerSubprovider]; + } else if (doesInjectedWeb3Exist && isPublicNodeAvailableForNetworkId) { // We catch all requests involving a users account and send it to the injectedWeb3 // instance. All other requests go to the public hosted node. - provider = new ProviderEngine(); + const provider = new ProviderEngine(); provider.addProvider(new InjectedWeb3Subprovider(injectedWeb3.currentProvider)); provider.addProvider(new FilterSubprovider()); const rpcSubproviders = _.map(publicNodeUrlsIfExistsForNetworkId, publicNodeUrl => { @@ -111,16 +160,17 @@ export class Blockchain { }); provider.addProvider(new RedundantSubprovider(rpcSubproviders as Subprovider[])); provider.start(); + return [provider, undefined]; } else if (doesInjectedWeb3Exist) { // Since no public node for this network, all requests go to injectedWeb3 instance - provider = injectedWeb3.currentProvider; + return [injectedWeb3.currentProvider, undefined]; } else { // If no injectedWeb3 instance, all requests fallback to our public hosted mainnet/testnet node // We do this so that users can still browse the 0x Portal DApp even if they do not have web3 // injected into their browser. - provider = new ProviderEngine(); + const provider = new ProviderEngine(); provider.addProvider(new FilterSubprovider()); - const networkId = configs.IS_MAINNET_ENABLED ? constants.NETWORK_ID_MAINNET : constants.NETWORK_ID_KOVAN; + const networkId = constants.NETWORK_ID_MAINNET; const rpcSubproviders = _.map(configs.PUBLIC_NODE_URLS_BY_NETWORK_ID[networkId], publicNodeUrl => { return new RpcSubprovider({ rpcUrl: publicNodeUrl, @@ -128,14 +178,15 @@ export class Blockchain { }); provider.addProvider(new RedundantSubprovider(rpcSubproviders as Subprovider[])); provider.start(); + return [provider, undefined]; } - - return provider; } constructor(dispatcher: Dispatcher) { this._dispatcher = dispatcher; const defaultGasPrice = GWEI_IN_WEI * 30; this._defaultGasPrice = new BigNumber(defaultGasPrice); + // We need a unique reference to this function so we can use it to unsubcribe. + this._injectedProviderUpdateHandler = this._handleInjectedProviderUpdateAsync.bind(this); // tslint:disable-next-line:no-floating-promises this._updateDefaultGasPriceAsync(); // tslint:disable-next-line:no-floating-promises @@ -185,84 +236,17 @@ export class Blockchain { this._ledgerSubprovider.setPath(path); } public async updateProviderToLedgerAsync(networkId: number): Promise<void> { - utils.assert(!_.isUndefined(this._contractWrappers), 'Contract Wrappers must be instantiated.'); - - 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 - if (_.isUndefined(this._cachedProvider)) { - this._cachedProvider = this._web3Wrapper.getProvider(); - this._cachedProviderNetworkId = this.networkId; - } - - this._blockchainWatcher.destroy(); - - delete this._userAddressIfExists; - this._dispatcher.updateUserAddress(undefined); // 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()); - const rpcSubproviders = _.map(configs.PUBLIC_NODE_URLS_BY_NETWORK_ID[networkId], publicNodeUrl => { - return new RpcSubprovider({ - rpcUrl: publicNodeUrl, - }); - }); - provider.addProvider(new RedundantSubprovider(rpcSubproviders as Subprovider[])); - provider.start(); - this.networkId = networkId; - this._dispatcher.updateNetworkId(this.networkId); const shouldPollUserAddress = false; - this._web3Wrapper = new Web3Wrapper(provider); - this._blockchainWatcher = new BlockchainWatcher( - this._dispatcher, - this._web3Wrapper, - this.networkId, - shouldPollUserAddress, - ); - this._contractWrappers.setProvider(provider, this.networkId); - await this._blockchainWatcher.startEmittingNetworkConnectionAndUserBalanceStateAsync(); - this._dispatcher.updateProviderType(ProviderType.Ledger); + const shouldUserLedgerProvider = true; + await this._resetOrInitializeAsync(networkId, shouldPollUserAddress, shouldUserLedgerProvider); } public async updateProviderToInjectedAsync(): Promise<void> { - utils.assert(!_.isUndefined(this._contractWrappers), 'Contract Wrappers must be instantiated.'); - - if (_.isUndefined(this._cachedProvider)) { - return; // Going from injected to injected, so we noop - } - - this._blockchainWatcher.destroy(); - - const provider = this._cachedProvider; - this.networkId = this._cachedProviderNetworkId; - const shouldPollUserAddress = true; - this._web3Wrapper = new Web3Wrapper(provider); - this._blockchainWatcher = new BlockchainWatcher( - this._dispatcher, - this._web3Wrapper, - this.networkId, - shouldPollUserAddress, - ); - - const userAddresses = await this._web3Wrapper.getAvailableAddressesAsync(); - this._userAddressIfExists = userAddresses[0]; - - this._contractWrappers.setProvider(provider, this.networkId); - - await this.fetchTokenInformationAsync(); - await this._blockchainWatcher.startEmittingNetworkConnectionAndUserBalanceStateAsync(); - this._dispatcher.updateProviderType(ProviderType.Injected); - delete this._ledgerSubprovider; - delete this._cachedProvider; + const shouldUserLedgerProvider = false; + this._dispatcher.updateBlockchainIsLoaded(false); + // We don't want to be out of sync with the network the injected provider declares. + const networkId = await Blockchain._getInjectedWeb3ProviderNetworkIdIfExistsAsync(); + await this._resetOrInitializeAsync(networkId, shouldPollUserAddress, shouldUserLedgerProvider); } public async setProxyAllowanceAsync(token: Token, amountInBaseUnits: BigNumber): Promise<void> { utils.assert(this.isValidAddress(token.address), BlockchainCallErrs.TokenAddressIsInvalid); @@ -538,6 +522,7 @@ export class Blockchain { } public destroy(): void { this._blockchainWatcher.destroy(); + this._injectedProviderObservable.unsubscribe(this._injectedProviderUpdateHandler); this._stopWatchingExchangeLogFillEvents(); } public async fetchTokenInformationAsync(): Promise<void> { @@ -559,6 +544,7 @@ export class Blockchain { tokenRegistryTokenSymbols, configs.DEFAULT_TRACKED_TOKEN_SYMBOLS, ); + const currentTimestamp = moment().unix(); if (defaultTrackedTokensInRegistry.length !== configs.DEFAULT_TRACKED_TOKEN_SYMBOLS.length) { this._dispatcher.updateShouldBlockchainErrDialogBeOpen(true); this._dispatcher.encounteredBlockchainError(BlockchainErrs.DefaultTokensNotInTokenRegistry); @@ -573,7 +559,7 @@ export class Blockchain { if (_.isEmpty(trackedTokensByAddress)) { _.each(configs.DEFAULT_TRACKED_TOKEN_SYMBOLS, symbol => { const token = _.find(tokenRegistryTokens, t => t.symbol === symbol); - token.isTracked = true; + token.trackedTimestamp = currentTimestamp; trackedTokensByAddress[token.address] = token; }); if (!_.isUndefined(this._userAddressIfExists)) { @@ -582,10 +568,10 @@ export class Blockchain { }); } } else { - // Properly set all tokenRegistry tokens `isTracked` to true if they are in the existing trackedTokens array - _.each(trackedTokensByAddress, (_trackedToken: Token, address: string) => { + // Properly set all tokenRegistry tokens `trackedTimestamp` if they are in the existing trackedTokens array + _.each(trackedTokensByAddress, (trackedToken: Token, address: string) => { if (!_.isUndefined(tokenRegistryTokensByAddress[address])) { - tokenRegistryTokensByAddress[address].isTracked = true; + tokenRegistryTokensByAddress[address].trackedTimestamp = trackedToken.trackedTimestamp; } }); } @@ -632,6 +618,18 @@ export class Blockchain { private _doesUserAddressExist(): boolean { return !_.isUndefined(this._userAddressIfExists); } + private async _handleInjectedProviderUpdateAsync(update: InjectedProviderUpdate): Promise<void> { + if (update.networkVersion === 'loading' || !_.isUndefined(this._ledgerSubprovider)) { + return; + } + const updatedNetworkId = _.parseInt(update.networkVersion); + if (this.networkId === updatedNetworkId) { + return; + } + const shouldPollUserAddress = true; + const shouldUserLedgerProvider = false; + await this._resetOrInitializeAsync(updatedNetworkId, shouldPollUserAddress, shouldUserLedgerProvider); + } private async _rehydrateStoreWithContractEventsAsync(): Promise<void> { // Ensure we are only ever listening to one set of events this._stopWatchingExchangeLogFillEvents(); @@ -765,7 +763,7 @@ export class Blockchain { name: t.name, symbol: t.symbol, decimals: t.decimals, - isTracked: false, + trackedTimestamp: undefined, isRegistered: true, }; tokenByAddress[token.address] = token; @@ -774,49 +772,64 @@ export class Blockchain { } private async _onPageLoadInitFireAndForgetAsync(): Promise<void> { await utils.onPageLoadAsync(); // wait for page to load - - // Hack: We need to know the networkId the injectedWeb3 is connected to (if it is defined) in - // order to properly instantiate the web3Wrapper. Since we must use the async call, we cannot - // retrieve it from within the web3Wrapper constructor. This is and should remain the only - // call to a web3 instance outside of web3Wrapper in the entire dapp. - // In addition, if the user has an injectedWeb3 instance that is disconnected from a backing - // Ethereum node, this call will throw. We need to handle this case gracefully - const injectedWeb3 = (window as any).web3; - let networkIdIfExists: number; - if (!_.isUndefined(injectedWeb3)) { - try { - networkIdIfExists = _.parseInt(await promisify<string>(injectedWeb3.version.getNetwork)()); - } catch (err) { - // Ignore error and proceed with networkId undefined + const networkIdIfExists = await Blockchain._getInjectedWeb3ProviderNetworkIdIfExistsAsync(); + this.networkId = !_.isUndefined(networkIdIfExists) ? networkIdIfExists : constants.NETWORK_ID_MAINNET; + const injectedWeb3IfExists = Blockchain._getInjectedWeb3(); + if (!_.isUndefined(injectedWeb3IfExists) && !_.isUndefined(injectedWeb3IfExists.currentProvider)) { + const injectedProviderObservable = injectedWeb3IfExists.currentProvider.publicConfigStore; + if (!_.isUndefined(injectedProviderObservable) && _.isUndefined(this._injectedProviderObservable)) { + this._injectedProviderObservable = injectedProviderObservable; + this._injectedProviderObservable.subscribe(this._injectedProviderUpdateHandler); } } - - const provider = await Blockchain._getProviderAsync(injectedWeb3, networkIdIfExists); - this.networkId = !_.isUndefined(networkIdIfExists) - ? networkIdIfExists - : configs.IS_MAINNET_ENABLED - ? constants.NETWORK_ID_MAINNET - : constants.NETWORK_ID_KOVAN; - this._dispatcher.updateNetworkId(this.networkId); - const zeroExConfigs = { - networkId: this.networkId, - }; - this._contractWrappers = new ContractWrappers(provider, zeroExConfigs); - this._updateProviderName(injectedWeb3); + this._updateProviderName(injectedWeb3IfExists); const shouldPollUserAddress = true; - this._web3Wrapper = new Web3Wrapper(provider); - this._blockchainWatcher = new BlockchainWatcher( - this._dispatcher, - this._web3Wrapper, - this.networkId, - shouldPollUserAddress, + const shouldUseLedgerProvider = false; + await this._resetOrInitializeAsync(this.networkId, shouldPollUserAddress, shouldUseLedgerProvider); + } + private async _resetOrInitializeAsync( + networkId: number, + shouldPollUserAddress: boolean = false, + shouldUserLedgerProvider: boolean = false, + ): Promise<void> { + if (!shouldUserLedgerProvider) { + this._dispatcher.updateBlockchainIsLoaded(false); + } + this._dispatcher.updateUserWeiBalance(undefined); + this.networkId = networkId; + const injectedWeb3IfExists = Blockchain._getInjectedWeb3(); + const [provider, ledgerSubproviderIfExists] = await Blockchain._getProviderAsync( + injectedWeb3IfExists, + networkId, + shouldUserLedgerProvider, ); - - const userAddresses = await this._web3Wrapper.getAvailableAddressesAsync(); - this._userAddressIfExists = userAddresses[0]; - this._dispatcher.updateUserAddress(this._userAddressIfExists); - await this.fetchTokenInformationAsync(); - await this._blockchainWatcher.startEmittingNetworkConnectionAndUserBalanceStateAsync(); + if (!_.isUndefined(this._contractWrappers)) { + this._contractWrappers.setProvider(provider, networkId); + } else { + this._contractWrappers = new ContractWrappers(provider, { networkId }); + } + if (!_.isUndefined(this._blockchainWatcher)) { + this._blockchainWatcher.destroy(); + } + this._web3Wrapper = new Web3Wrapper(provider); + this._blockchainWatcher = new BlockchainWatcher(this._dispatcher, this._web3Wrapper, shouldPollUserAddress); + if (shouldUserLedgerProvider && !_.isUndefined(ledgerSubproviderIfExists)) { + delete this._userAddressIfExists; + this._ledgerSubprovider = ledgerSubproviderIfExists; + this._dispatcher.updateUserAddress(undefined); + this._dispatcher.updateProviderType(ProviderType.Ledger); + } else { + delete this._ledgerSubprovider; + const userAddresses = await this._web3Wrapper.getAvailableAddressesAsync(); + this._userAddressIfExists = userAddresses[0]; + this._dispatcher.updateUserAddress(this._userAddressIfExists); + if (!_.isUndefined(injectedWeb3IfExists)) { + this._dispatcher.updateProviderType(ProviderType.Injected); + } + await this.fetchTokenInformationAsync(); + } + await this._blockchainWatcher.startEmittingUserBalanceStateAsync(); + this._dispatcher.updateNetworkId(networkId); await this._rehydrateStoreWithContractEventsAsync(); } private _updateProviderName(injectedWeb3: Web3): void { diff --git a/packages/website/ts/blockchain_watcher.ts b/packages/website/ts/blockchain_watcher.ts index c576db6ac..df5f73fd1 100644 --- a/packages/website/ts/blockchain_watcher.ts +++ b/packages/website/ts/blockchain_watcher.ts @@ -6,24 +6,17 @@ import { Dispatcher } from 'ts/redux/dispatcher'; export class BlockchainWatcher { private _dispatcher: Dispatcher; private _web3Wrapper: Web3Wrapper; - private _prevNetworkId: number; private _shouldPollUserAddress: boolean; - private _watchNetworkAndBalanceIntervalId: NodeJS.Timer; + private _watchBalanceIntervalId: NodeJS.Timer; private _prevUserEtherBalanceInWei?: BigNumber; private _prevUserAddressIfExists: string; - constructor( - dispatcher: Dispatcher, - web3Wrapper: Web3Wrapper, - networkIdIfExists: number, - shouldPollUserAddress: boolean, - ) { + constructor(dispatcher: Dispatcher, web3Wrapper: Web3Wrapper, shouldPollUserAddress: boolean) { this._dispatcher = dispatcher; - this._prevNetworkId = networkIdIfExists; this._shouldPollUserAddress = shouldPollUserAddress; this._web3Wrapper = web3Wrapper; } public destroy(): void { - this._stopEmittingNetworkConnectionAndUserBalanceState(); + this._stopEmittingUserBalanceState(); // HACK: stop() is only available on providerEngine instances const provider = this._web3Wrapper.getProvider(); if (!_.isUndefined((provider as any).stop)) { @@ -34,36 +27,23 @@ export class BlockchainWatcher { public updatePrevUserAddress(userAddress: string): void { this._prevUserAddressIfExists = userAddress; } - public async startEmittingNetworkConnectionAndUserBalanceStateAsync(): Promise<void> { - if (!_.isUndefined(this._watchNetworkAndBalanceIntervalId)) { + public async startEmittingUserBalanceStateAsync(): Promise<void> { + if (!_.isUndefined(this._watchBalanceIntervalId)) { return; // we are already emitting the state } this._prevUserEtherBalanceInWei = undefined; - this._dispatcher.updateNetworkId(this._prevNetworkId); - await this._updateNetworkAndBalanceAsync(); - this._watchNetworkAndBalanceIntervalId = intervalUtils.setAsyncExcludingInterval( - this._updateNetworkAndBalanceAsync.bind(this), + await this._updateBalanceAsync(); + this._watchBalanceIntervalId = intervalUtils.setAsyncExcludingInterval( + this._updateBalanceAsync.bind(this), 5000, (err: Error) => { logUtils.log(`Watching network and balances failed: ${err.stack}`); - this._stopEmittingNetworkConnectionAndUserBalanceState(); + this._stopEmittingUserBalanceState(); }, ); } - private async _updateNetworkAndBalanceAsync(): Promise<void> { - // Check for network state changes + private async _updateBalanceAsync(): Promise<void> { let prevNodeVersion: string; - let currentNetworkId; - try { - currentNetworkId = await this._web3Wrapper.getNetworkIdAsync(); - } catch (err) { - // Noop - } - if (currentNetworkId !== this._prevNetworkId) { - this._prevNetworkId = currentNetworkId; - this._dispatcher.updateNetworkId(currentNetworkId); - } - // Check for node version changes const currentNodeVersion = await this._web3Wrapper.getNodeVersionAsync(); if (currentNodeVersion !== prevNodeVersion) { @@ -99,9 +79,9 @@ export class BlockchainWatcher { this._dispatcher.updateUserWeiBalance(balanceInWei); } } - private _stopEmittingNetworkConnectionAndUserBalanceState(): void { - if (!_.isUndefined(this._watchNetworkAndBalanceIntervalId)) { - intervalUtils.clearAsyncExcludingInterval(this._watchNetworkAndBalanceIntervalId); + private _stopEmittingUserBalanceState(): void { + if (!_.isUndefined(this._watchBalanceIntervalId)) { + intervalUtils.clearAsyncExcludingInterval(this._watchBalanceIntervalId); } } } diff --git a/packages/website/ts/components/dialogs/blockchain_err_dialog.tsx b/packages/website/ts/components/dialogs/blockchain_err_dialog.tsx index b968a3147..c8e10303f 100644 --- a/packages/website/ts/components/dialogs/blockchain_err_dialog.tsx +++ b/packages/website/ts/components/dialogs/blockchain_err_dialog.tsx @@ -4,7 +4,6 @@ import FlatButton from 'material-ui/FlatButton'; import * as React from 'react'; import { Blockchain } from 'ts/blockchain'; import { BlockchainErrs } from 'ts/types'; -import { configs } from 'ts/utils/configs'; import { constants } from 'ts/utils/constants'; interface BlockchainErrDialogProps { @@ -125,8 +124,7 @@ export class BlockchainErrDialog extends React.Component<BlockchainErrDialogProp Parity Signer Chrome extension </a>{' '} lets you connect to a locally running Parity node. Make sure you have started your local Parity node - with {configs.IS_MAINNET_ENABLED && '`parity ui` or'} `parity --chain kovan ui` in order to connect - to {configs.IS_MAINNET_ENABLED ? 'mainnet or Kovan respectively.' : 'Kovan.'} + with `parity ui` or `parity --chain kovan ui` in order to connect to mainnet or Kovan respectively. </div> <div className="pt2"> <span className="bold">Note:</span> If you have done one of the above steps and are still seeing @@ -142,10 +140,8 @@ export class BlockchainErrDialog extends React.Component<BlockchainErrDialogProp <div> The 0x smart contracts are not deployed on the Ethereum network you are currently connected to (network Id: {this.props.networkId}). In order to use the 0x portal dApp, please connect to the{' '} - {Networks.Kovan} testnet (network Id: {constants.NETWORK_ID_KOVAN}) - {configs.IS_MAINNET_ENABLED - ? ` or ${constants.MAINNET_NAME} (network Id: ${constants.NETWORK_ID_MAINNET}).` - : `.`} + {Networks.Kovan} testnet (network Id: {constants.NETWORK_ID_KOVAN}) or ${constants.MAINNET_NAME}{' '} + (network Id: ${constants.NETWORK_ID_MAINNET}). </div> <h4>Metamask</h4> <div> @@ -159,11 +155,8 @@ export class BlockchainErrDialog extends React.Component<BlockchainErrDialogProp If using the{' '} <a href={constants.URL_PARITY_CHROME_STORE} target="_blank"> Parity Signer Chrome extension - </a>, make sure to start your local Parity node with{' '} - {configs.IS_MAINNET_ENABLED - ? '`parity ui` or `parity --chain Kovan ui` in order to connect to mainnet \ - or Kovan respectively.' - : '`parity --chain kovan ui` in order to connect to Kovan.'} + </a>, make sure to start your local Parity node with `parity ui` or `parity --chain Kovan ui` in + order to connect to mainnet \ or Kovan respectively. </div> </div> ); diff --git a/packages/website/ts/components/dialogs/ledger_config_dialog.tsx b/packages/website/ts/components/dialogs/ledger_config_dialog.tsx index c9727b553..38e4732a4 100644 --- a/packages/website/ts/components/dialogs/ledger_config_dialog.tsx +++ b/packages/website/ts/components/dialogs/ledger_config_dialog.tsx @@ -282,6 +282,7 @@ export class LedgerConfigDialog extends React.Component<LedgerConfigDialogProps, if (didSucceed) { this.setState({ stepIndex: LedgerSteps.SELECT_ADDRESS, + connectionErrMsg: '', }); } return didSucceed; diff --git a/packages/website/ts/components/dialogs/track_token_confirmation_dialog.tsx b/packages/website/ts/components/dialogs/track_token_confirmation_dialog.tsx index f6594b9d5..3751ce06f 100644 --- a/packages/website/ts/components/dialogs/track_token_confirmation_dialog.tsx +++ b/packages/website/ts/components/dialogs/track_token_confirmation_dialog.tsx @@ -1,5 +1,6 @@ import Dialog from 'material-ui/Dialog'; import FlatButton from 'material-ui/FlatButton'; +import * as moment from 'moment'; import * as React from 'react'; import { Blockchain } from 'ts/blockchain'; import { TrackTokenConfirmation } from 'ts/components/track_token_confirmation'; @@ -73,12 +74,13 @@ export class TrackTokenConfirmationDialog extends React.Component< this.setState({ isAddingTokenToTracked: true, }); + const currentTimestamp = moment().unix(); for (const token of this.props.tokens) { const newTokenEntry = { ...token, + trackedTimestamp: currentTimestamp, }; - newTokenEntry.isTracked = true; trackedTokenStorage.addTrackedTokenToUser(this.props.userAddress, this.props.networkId, newTokenEntry); this.props.dispatcher.updateTokenByAddress([newTokenEntry]); } diff --git a/packages/website/ts/components/fill_order.tsx b/packages/website/ts/components/fill_order.tsx index f3ea44286..03ba1183d 100644 --- a/packages/website/ts/components/fill_order.tsx +++ b/packages/website/ts/components/fill_order.tsx @@ -373,26 +373,26 @@ export class FillOrder extends React.Component<FillOrderProps, FillOrderState> { const tokensToTrack: Token[] = []; const isUnseenMakerToken = _.isUndefined(makerTokenIfExists); - const isMakerTokenTracked = !_.isUndefined(makerTokenIfExists) && makerTokenIfExists.isTracked; + const isMakerTokenTracked = !_.isUndefined(makerTokenIfExists) && utils.isTokenTracked(makerTokenIfExists); if (isUnseenMakerToken) { tokensToTrack.push({ ...this.state.parsedOrder.metadata.makerToken, address: this.state.parsedOrder.signedOrder.makerTokenAddress, iconUrl: undefined, - isTracked: false, + trackedTimestamp: undefined, isRegistered: false, }); } else if (!isMakerTokenTracked) { tokensToTrack.push(makerTokenIfExists); } const isUnseenTakerToken = _.isUndefined(takerTokenIfExists); - const isTakerTokenTracked = !_.isUndefined(takerTokenIfExists) && takerTokenIfExists.isTracked; + const isTakerTokenTracked = !_.isUndefined(takerTokenIfExists) && utils.isTokenTracked(takerTokenIfExists); if (isUnseenTakerToken) { tokensToTrack.push({ ...this.state.parsedOrder.metadata.takerToken, address: this.state.parsedOrder.signedOrder.takerTokenAddress, iconUrl: undefined, - isTracked: false, + trackedTimestamp: undefined, isRegistered: false, }); } else if (!isTakerTokenTracked) { diff --git a/packages/website/ts/components/generate_order/asset_picker.tsx b/packages/website/ts/components/generate_order/asset_picker.tsx index b0dcf5678..3d53a9e7d 100644 --- a/packages/website/ts/components/generate_order/asset_picker.tsx +++ b/packages/website/ts/components/generate_order/asset_picker.tsx @@ -1,6 +1,7 @@ import * as _ from 'lodash'; import Dialog from 'material-ui/Dialog'; import FlatButton from 'material-ui/FlatButton'; +import * as moment from 'moment'; import * as React from 'react'; import { Blockchain } from 'ts/blockchain'; import { NewTokenForm } from 'ts/components/generate_order/new_token_form'; @@ -10,6 +11,7 @@ import { trackedTokenStorage } from 'ts/local_storage/tracked_token_storage'; import { Dispatcher } from 'ts/redux/dispatcher'; import { DialogConfigs, Token, TokenByAddress, TokenVisibility } from 'ts/types'; import { constants } from 'ts/utils/constants'; +import { utils } from 'ts/utils/utils'; const TOKEN_ICON_DIMENSION = 100; const TILE_DIMENSION = 146; @@ -117,7 +119,7 @@ export class AssetPicker extends React.Component<AssetPickerProps, AssetPickerSt private _renderAssetPicker(): React.ReactNode { return ( <div - className="clearfix flex flex-wrap" + className="flex flex-wrap" style={{ overflowY: 'auto', maxWidth: 720, @@ -134,8 +136,8 @@ export class AssetPicker extends React.Component<AssetPickerProps, AssetPickerSt let tileStyles; const gridTiles = _.map(this.props.tokenByAddress, (token: Token, address: string) => { if ( - (this.props.tokenVisibility === TokenVisibility.TRACKED && !token.isTracked) || - (this.props.tokenVisibility === TokenVisibility.UNTRACKED && token.isTracked) || + (this.props.tokenVisibility === TokenVisibility.TRACKED && !utils.isTokenTracked(token)) || + (this.props.tokenVisibility === TokenVisibility.UNTRACKED && utils.isTokenTracked(token)) || token.symbol === constants.ZRX_TOKEN_SYMBOL || token.symbol === constants.ETHER_TOKEN_SYMBOL ) { @@ -154,7 +156,7 @@ export class AssetPicker extends React.Component<AssetPickerProps, AssetPickerSt height: TILE_DIMENSION, ...tileStyles, }} - className="p2 flex flex-column items-center" + className="p2 flex sm-col-6 md-col-3 lg-col-3 flex-column items-center mx-auto" onClick={this._onChooseToken.bind(this, address)} onMouseEnter={this._onToggleHover.bind(this, address, true)} onMouseLeave={this._onToggleHover.bind(this, address, false)} @@ -162,7 +164,7 @@ export class AssetPicker extends React.Component<AssetPickerProps, AssetPickerSt <div className="p1"> <TokenIcon token={token} diameter={TOKEN_ICON_DIMENSION} /> </div> - <div className="center">{token.name}</div> + <div className="center">{token.symbol}</div> </div> ); }); @@ -181,7 +183,7 @@ export class AssetPicker extends React.Component<AssetPickerProps, AssetPickerSt height: TILE_DIMENSION, ...tileStyles, }} - className="p2 mx-auto" + className="p2 flex sm-col-6 md-col-3 lg-col-3 flex-column items-center mx-auto" onClick={this._onCustomAssetChosen.bind(this)} onMouseEnter={this._onToggleHover.bind(this, otherTokenKey, true)} onMouseLeave={this._onToggleHover.bind(this, otherTokenKey, false)} @@ -212,7 +214,7 @@ export class AssetPicker extends React.Component<AssetPickerProps, AssetPickerSt } private _onChooseToken(tokenAddress: string): void { const token = this.props.tokenByAddress[tokenAddress]; - if (token.isTracked) { + if (utils.isTokenTracked(token)) { this.props.onTokenChosen(tokenAddress); } else { this.setState({ @@ -257,9 +259,8 @@ export class AssetPicker extends React.Component<AssetPickerProps, AssetPickerSt } const newTokenEntry = { ...token, + trackedTimestamp: moment().unix(), }; - - newTokenEntry.isTracked = true; trackedTokenStorage.addTrackedTokenToUser(this.props.userAddress, this.props.networkId, newTokenEntry); this.props.dispatcher.updateTokenByAddress([newTokenEntry]); diff --git a/packages/website/ts/components/generate_order/new_token_form.tsx b/packages/website/ts/components/generate_order/new_token_form.tsx index 176a0807b..3d7eda84c 100644 --- a/packages/website/ts/components/generate_order/new_token_form.tsx +++ b/packages/website/ts/components/generate_order/new_token_form.tsx @@ -1,6 +1,7 @@ import { colors } from '@0xproject/react-shared'; import * as _ from 'lodash'; import TextField from 'material-ui/TextField'; +import * as moment from 'moment'; import * as React from 'react'; import { Blockchain } from 'ts/blockchain'; import { AddressInput } from 'ts/components/inputs/address_input'; @@ -147,7 +148,7 @@ export class NewTokenForm extends React.Component<NewTokenFormProps, NewTokenFor iconUrl: undefined, name: this.state.name, symbol: this.state.symbol.toUpperCase(), - isTracked: true, + trackedTimestamp: moment().unix(), isRegistered: false, }; this.props.onNewTokenSubmitted(newToken); diff --git a/packages/website/ts/components/inputs/allowance_toggle.tsx b/packages/website/ts/components/inputs/allowance_toggle.tsx index 0dd2a5aa5..0d5995696 100644 --- a/packages/website/ts/components/inputs/allowance_toggle.tsx +++ b/packages/website/ts/components/inputs/allowance_toggle.tsx @@ -48,10 +48,10 @@ const styles: Styles = { width: 25, }, offTrackStyle: { - backgroundColor: colors.allowanceToggleOffTrack, + backgroundColor: colors.grey300, }, onTrackStyle: { - backgroundColor: colors.allowanceToggleOnTrack, + backgroundColor: colors.mediumBlue, }, }; diff --git a/packages/website/ts/components/legacy_portal/legacy_portal.tsx b/packages/website/ts/components/legacy_portal/legacy_portal.tsx index b4a174a03..c85d97207 100644 --- a/packages/website/ts/components/legacy_portal/legacy_portal.tsx +++ b/packages/website/ts/components/legacy_portal/legacy_portal.tsx @@ -23,7 +23,6 @@ import { GenerateOrderForm } from 'ts/containers/generate_order_form'; import { localStorage } from 'ts/local_storage/local_storage'; import { Dispatcher } from 'ts/redux/dispatcher'; import { BlockchainErrs, HashData, Order, ProviderType, ScreenWidths, TokenByAddress, WebsitePaths } from 'ts/types'; -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'; @@ -170,67 +169,53 @@ export class LegacyPortal extends React.Component<LegacyPortalProps, LegacyPorta /> <div id="portal" className="mx-auto max-width-4" style={{ width: '100%' }}> <Paper className="mb3 mt2"> - {!configs.IS_MAINNET_ENABLED && this.props.networkId === constants.NETWORK_ID_MAINNET ? ( - <div className="p3 center"> - <div className="h2 py2">Mainnet unavailable</div> - <div className="mx-auto pb2 pt2"> - <img src="/images/zrx_token.png" style={{ width: 150 }} /> - </div> - <div> - 0x portal is currently unavailable on the Ethereum mainnet. - <div>To try it out, switch to the Kovan test network (networkId: 42).</div> - <div className="py2">Check back soon!</div> - </div> + <div className="mx-auto flex"> + <div className="col col-2 pr2 pt1 sm-hide xs-hide" style={portalMenuContainerStyle}> + <LegacyPortalMenu menuItemStyle={{ color: colors.white }} /> </div> - ) : ( - <div className="mx-auto flex"> - <div className="col col-2 pr2 pt1 sm-hide xs-hide" style={portalMenuContainerStyle}> - <LegacyPortalMenu menuItemStyle={{ color: colors.white }} /> - </div> - <div className="col col-12 lg-col-10 md-col-10 sm-col sm-col-12"> - <div className="py2" style={{ backgroundColor: colors.grey50 }}> - {this.props.blockchainIsLoaded ? ( - <Switch> - <Route - path={`${WebsitePaths.Portal}/weth`} - render={this._renderEthWrapper.bind(this)} - /> - <Route - path={`${WebsitePaths.Portal}/fill`} - render={this._renderFillOrder.bind(this)} - /> - <Route - path={`${WebsitePaths.Portal}/balances`} - render={this._renderTokenBalances.bind(this)} - /> - <Route - path={`${WebsitePaths.Portal}/trades`} - render={this._renderTradeHistory.bind(this)} - /> - <Route - path={`${WebsitePaths.Home}`} - render={this._renderGenerateOrderForm.bind(this)} - /> - </Switch> - ) : ( - <div className="pt4 sm-px2 sm-pt2 sm-m1" style={{ height: 500 }}> - <div - className="relative sm-px2 sm-pt2 sm-m1" - style={{ height: 122, top: '50%', transform: 'translateY(-50%)' }} - > - <div className="center pb2"> - <CircularProgress size={40} thickness={5} /> - </div> - <div className="center pt2" style={{ paddingBottom: 11 }}> - Loading Portal... - </div> + <div className="col col-12 lg-col-10 md-col-10 sm-col sm-col-12"> + <div className="py2" style={{ backgroundColor: colors.grey50 }}> + {this.props.blockchainIsLoaded ? ( + <Switch> + <Route + path={`${WebsitePaths.Portal}/weth`} + render={this._renderEthWrapper.bind(this)} + /> + <Route + path={`${WebsitePaths.Portal}/fill`} + render={this._renderFillOrder.bind(this)} + /> + <Route + path={`${WebsitePaths.Portal}/balances`} + render={this._renderTokenBalances.bind(this)} + /> + <Route + path={`${WebsitePaths.Portal}/trades`} + render={this._renderTradeHistory.bind(this)} + /> + <Route + path={`${WebsitePaths.Home}`} + render={this._renderGenerateOrderForm.bind(this)} + /> + </Switch> + ) : ( + <div className="pt4 sm-px2 sm-pt2 sm-m1" style={{ height: 500 }}> + <div + className="relative sm-px2 sm-pt2 sm-m1" + style={{ height: 122, top: '50%', transform: 'translateY(-50%)' }} + > + <div className="center pb2"> + <CircularProgress size={40} thickness={5} /> + </div> + <div className="center pt2" style={{ paddingBottom: 11 }}> + Loading Portal... </div> </div> - )} - </div> + </div> + )} </div> </div> - )} + </div> </Paper> <BlockchainErrDialog blockchain={this._blockchain} @@ -249,16 +234,14 @@ export class LegacyPortal extends React.Component<LegacyPortalProps, LegacyPorta 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} - /> - )} + <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 translate={this.props.translate} dispatcher={this.props.dispatcher} /> </div> @@ -292,8 +275,7 @@ export class LegacyPortal extends React.Component<LegacyPortalProps, LegacyPorta ); } private _renderTokenBalances(): React.ReactNode { - const allTokens = _.values(this.props.tokenByAddress); - const trackedTokens = _.filter(allTokens, t => t.isTracked); + const trackedTokens = utils.getTrackedTokens(this.props.tokenByAddress); return ( <TokenBalances blockchain={this._blockchain} diff --git a/packages/website/ts/components/onboarding/add_eth_onboarding_step.tsx b/packages/website/ts/components/onboarding/add_eth_onboarding_step.tsx index 31ce99d31..bccdc0c18 100644 --- a/packages/website/ts/components/onboarding/add_eth_onboarding_step.tsx +++ b/packages/website/ts/components/onboarding/add_eth_onboarding_step.tsx @@ -1,18 +1,42 @@ +import { BigNumber } from '@0xproject/utils'; import * as React from 'react'; import { Container } from 'ts/components/ui/container'; +import { Image } from 'ts/components/ui/image'; import { Text } from 'ts/components/ui/text'; +import { constants } from 'ts/utils/constants'; +import { utils } from 'ts/utils/utils'; -export interface AddEthOnboardingStepProps {} +export interface AddEthOnboardingStepProps { + userEthBalanceInWei: BigNumber; +} -export const AddEthOnboardingStep: React.StatelessComponent<AddEthOnboardingStepProps> = () => ( - <div className="flex items-center flex-column"> - <Text> Before you begin you will need to send some ETH to your metamask wallet.</Text> - <Container marginTop="15px" marginBottom="15px"> - <img src="/images/ether_alt.svg" height="50px" width="50px" /> - </Container> - <Text> - Click on the <img src="/images/metamask_icon.png" height="20px" width="20px" /> metamask extension in your - browser and click either <b>BUY</b> or <b>DEPOSIT</b>. - </Text> - </div> -); +export const AddEthOnboardingStep: React.StatelessComponent<AddEthOnboardingStepProps> = props => + props.userEthBalanceInWei.gt(0) ? ( + <div className="flex items-center flex-column"> + <Text> + Great! Looks like you already have{' '} + <b> + {utils.getFormattedAmount( + props.userEthBalanceInWei, + constants.DECIMAL_PLACES_ETH, + constants.ETHER_SYMBOL, + )}{' '} + </b> + in your wallet. + </Text> + <Container marginTop="15px" marginBottom="15px"> + <Image src="/images/ether_alt.svg" height="50px" width="50px" /> + </Container> + </div> + ) : ( + <div className="flex items-center flex-column"> + <Text> Before you begin you will need to send some ETH to your wallet.</Text> + <Container marginTop="15px" marginBottom="15px"> + <Image src="/images/ether_alt.svg" height="50px" width="50px" /> + </Container> + <Text className="xs-hide"> + Click on the <Image src="/images/metamask_icon.png" height="20px" width="20px" /> MetaMask extension in + your browser and click either <b>BUY</b> or <b>DEPOSIT</b>. + </Text> + </div> + ); diff --git a/packages/website/ts/components/onboarding/congrats_onboarding_step.tsx b/packages/website/ts/components/onboarding/congrats_onboarding_step.tsx index 3a8db8c36..8100fd2c0 100644 --- a/packages/website/ts/components/onboarding/congrats_onboarding_step.tsx +++ b/packages/website/ts/components/onboarding/congrats_onboarding_step.tsx @@ -10,6 +10,6 @@ export const CongratsOnboardingStep: React.StatelessComponent<CongratsOnboarding <Container marginTop="25px" marginBottom="15px" className="flex justify-center"> <img src="/images/zrx_ecosystem.svg" height="150px" /> </Container> - <Text>No need to log in. Each relayer automatically detects and connects to your metamask wallet.</Text> + <Text>No need to log in. Each relayer automatically detects and connects to your wallet.</Text> </div> ); diff --git a/packages/website/ts/components/onboarding/install_wallet_onboarding_step.tsx b/packages/website/ts/components/onboarding/install_wallet_onboarding_step.tsx index a54496186..a95c464af 100644 --- a/packages/website/ts/components/onboarding/install_wallet_onboarding_step.tsx +++ b/packages/website/ts/components/onboarding/install_wallet_onboarding_step.tsx @@ -8,11 +8,12 @@ export interface InstallWalletOnboardingStepProps {} export const InstallWalletOnboardingStep: React.StatelessComponent<InstallWalletOnboardingStepProps> = () => ( <div className="flex items-center flex-column"> - <Container marginTop="15px" marginBottom="15px"> - <ActionAccountBalanceWallet style={{ width: '30px', height: '30px' }} color={colors.orange} /> - </Container> <Text> Before you begin, you need to connect to a wallet. This will be used across all 0x relayers and dApps. </Text> + <Container marginTop="15px" marginBottom="15px"> + <ActionAccountBalanceWallet style={{ width: '50px', height: '50px' }} color={colors.orange} /> + </Container> + <Text>Please refresh the page once you've done this to continue!</Text> </div> ); diff --git a/packages/website/ts/components/onboarding/intro_onboarding_step.tsx b/packages/website/ts/components/onboarding/intro_onboarding_step.tsx index 548839218..3a27b6854 100644 --- a/packages/website/ts/components/onboarding/intro_onboarding_step.tsx +++ b/packages/website/ts/components/onboarding/intro_onboarding_step.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; import { Container } from 'ts/components/ui/container'; +import { Image } from 'ts/components/ui/image'; import { Text } from 'ts/components/ui/text'; export interface IntroOnboardingStepProps {} @@ -7,15 +8,19 @@ export interface IntroOnboardingStepProps {} export const IntroOnboardingStep: React.StatelessComponent<IntroOnboardingStepProps> = () => ( <div className="flex items-center flex-column"> <Text> - In order to start trading on any 0x relayer in the 0x ecosystem, you need to complete two simple steps. + In order to start trading on any 0x relayer in the 0x ecosystem, you need to complete three simple steps. </Text> <Container width="100%" marginTop="25px" marginBottom="15px" className="flex justify-around"> <div className="flex flex-column items-center"> - <img src="/images/eth_token.svg" height="50px" width="50x" /> + <Image src="/images/ether.png" height="50px" width="50px" /> + <Text> Add ETH </Text> + </div> + <div className="flex flex-column items-center"> + <Image src="/images/eth_token.svg" height="50px" width="50x" /> <Text> Wrap ETH </Text> </div> <div className="flex flex-column items-center"> - <img src="/images/fake_toggle.svg" height="50px" width="50px" /> + <Image src="/images/fake_toggle.svg" height="50px" width="50px" /> <Text> Unlock tokens </Text> </div> </Container> diff --git a/packages/website/ts/components/onboarding/onboarding_card.tsx b/packages/website/ts/components/onboarding/onboarding_card.tsx index bc83b8034..48e8ab022 100644 --- a/packages/website/ts/components/onboarding/onboarding_card.tsx +++ b/packages/website/ts/components/onboarding/onboarding_card.tsx @@ -1,6 +1,7 @@ import { colors } from '@0xproject/react-shared'; import * as React from 'react'; +import * as _ from 'lodash'; import { Button } from 'ts/components/ui/button'; import { Container } from 'ts/components/ui/container'; import { IconButton } from 'ts/components/ui/icon_button'; @@ -16,6 +17,7 @@ export interface OnboardingCardProps { onClose: () => void; onClickNext: () => void; onClickBack: () => void; + onContinueButtonClick?: () => void; continueButtonDisplay?: ContinueButtonDisplay; shouldHideBackButton?: boolean; shouldHideNextButton?: boolean; @@ -28,6 +30,7 @@ export const OnboardingCard: React.StatelessComponent<OnboardingCardProps> = ({ content, continueButtonDisplay, continueButtonText, + onContinueButtonClick, onClickNext, onClickBack, onClose, @@ -52,7 +55,7 @@ export const OnboardingCard: React.StatelessComponent<OnboardingCardProps> = ({ {continueButtonDisplay && ( <Button isDisabled={continueButtonDisplay === 'disabled'} - onClick={onClickNext} + onClick={!_.isUndefined(onContinueButtonClick) ? onContinueButtonClick : onClickNext} fontColor={colors.white} fontSize="15px" backgroundColor={colors.mediumBlue} @@ -60,17 +63,21 @@ export const OnboardingCard: React.StatelessComponent<OnboardingCardProps> = ({ {continueButtonText} </Button> )} - <Container className="flex justify-between" marginTop="15px"> - {!shouldHideBackButton && ( - <Text fontColor={colors.grey} onClick={onClickBack}> - Back - </Text> - )} - {!shouldHideNextButton && ( - <Text fontColor={colors.grey} onClick={onClickNext}> - Skip - </Text> - )} + <Container className="clearfix" marginTop="15px"> + <div className="left"> + {!shouldHideBackButton && ( + <Text fontColor={colors.grey} onClick={onClickBack}> + Back + </Text> + )} + </div> + <div className="right"> + {!shouldHideNextButton && ( + <Text fontColor={colors.grey} onClick={onClickNext}> + Skip + </Text> + )} + </div> </Container> </div> </Container> diff --git a/packages/website/ts/components/onboarding/onboarding_flow.tsx b/packages/website/ts/components/onboarding/onboarding_flow.tsx index ec8d96191..1f4c6df82 100644 --- a/packages/website/ts/components/onboarding/onboarding_flow.tsx +++ b/packages/website/ts/components/onboarding/onboarding_flow.tsx @@ -6,6 +6,7 @@ import { ContinueButtonDisplay, OnboardingTooltip } from 'ts/components/onboardi import { Animation } from 'ts/components/ui/animation'; import { Container } from 'ts/components/ui/container'; import { Overlay } from 'ts/components/ui/overlay'; +import { zIndex } from 'ts/style/z_index'; export interface Step { target: string; @@ -16,6 +17,7 @@ export interface Step { shouldHideNextButton?: boolean; continueButtonDisplay?: ContinueButtonDisplay; continueButtonText?: string; + onContinueButtonClick?: () => void; } export interface OnboardingFlowProps { @@ -54,14 +56,22 @@ export class OnboardingFlow extends React.Component<OnboardingFlowProps> { if (this.props.disableOverlay) { return onboardingElement; } - return <Overlay>{onboardingElement}</Overlay>; + return ( + <div> + <Overlay onClick={this.props.onClose} /> + {onboardingElement} + </div> + ); } private _getElementForStep(): Element { return document.querySelector(this._getCurrentStep().target); } private _renderPopperChildren(props: PopperChildrenProps): React.ReactNode { + const customStyles = { zIndex: zIndex.aboveOverlay }; + // On re-render, we want to re-center the popper. + props.scheduleUpdate(); return ( - <div ref={props.ref} style={props.style} data-placement={props.placement}> + <div ref={props.ref} style={{ ...props.style, ...customStyles }} data-placement={props.placement}> {this._renderToolTip()} </div> ); @@ -71,7 +81,7 @@ export class OnboardingFlow extends React.Component<OnboardingFlowProps> { const step = steps[stepIndex]; const isLastStep = steps.length - 1 === stepIndex; return ( - <Container marginLeft="30px" maxWidth={350}> + <Container marginLeft="30px" width="400px"> <OnboardingTooltip title={step.title} content={step.content} @@ -83,6 +93,7 @@ export class OnboardingFlow extends React.Component<OnboardingFlowProps> { onClickBack={this._goToPrevStep.bind(this)} continueButtonDisplay={step.continueButtonDisplay} continueButtonText={step.continueButtonText} + onContinueButtonClick={step.onContinueButtonClick} /> </Container> ); @@ -93,7 +104,7 @@ export class OnboardingFlow extends React.Component<OnboardingFlowProps> { const step = steps[stepIndex]; const isLastStep = steps.length - 1 === stepIndex; return ( - <Container position="relative" zIndex={1} maxWidth="100vw"> + <Container position="relative" zIndex={1}> <OnboardingCard title={step.title} content={step.content} @@ -105,6 +116,7 @@ export class OnboardingFlow extends React.Component<OnboardingFlowProps> { onClickBack={this._goToPrevStep.bind(this)} continueButtonDisplay={step.continueButtonDisplay} continueButtonText={step.continueButtonText} + onContinueButtonClick={step.onContinueButtonClick} borderRadius="10px 10px 0px 0px" /> </Container> diff --git a/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx b/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx index 10d4af30e..6bfa5c75f 100644 --- a/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx +++ b/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx @@ -12,7 +12,11 @@ import { IntroOnboardingStep } from 'ts/components/onboarding/intro_onboarding_s import { OnboardingFlow, Step } from 'ts/components/onboarding/onboarding_flow'; import { SetAllowancesOnboardingStep } from 'ts/components/onboarding/set_allowances_onboarding_step'; import { UnlockWalletOnboardingStep } from 'ts/components/onboarding/unlock_wallet_onboarding_step'; -import { WrapEthOnboardingStep } from 'ts/components/onboarding/wrap_eth_onboarding_step'; +import { + WrapEthOnboardingStep1, + WrapEthOnboardingStep2, + WrapEthOnboardingStep3, +} from 'ts/components/onboarding/wrap_eth_onboarding_step'; import { AllowanceToggle } from 'ts/containers/inputs/allowance_toggle'; import { ProviderType, ScreenWidths, Token, TokenByAddress, TokenStateByAddress } from 'ts/types'; import { analytics } from 'ts/utils/analytics'; @@ -24,7 +28,7 @@ export interface PortalOnboardingFlowProps extends RouteComponentProps<any> { stepIndex: number; isRunning: boolean; userAddress: string; - hasBeenSeen: boolean; + hasBeenClosed: boolean; providerType: ProviderType; injectedProviderName: string; blockchainIsLoaded: boolean; @@ -40,15 +44,23 @@ export interface PortalOnboardingFlowProps extends RouteComponentProps<any> { class PlainPortalOnboardingFlow extends React.Component<PortalOnboardingFlowProps> { private _unlisten: () => void; public componentDidMount(): void { - this._overrideOnboardingStateIfShould(); + this._adjustStepIfShould(); + // Wait until the step is adjusted to decide whether we should show onboarding. + setTimeout(this._autoStartOnboardingIfShould.bind(this), 1000); // If there is a route change, just close onboarding. this._unlisten = this.props.history.listen(() => this.props.updateIsRunning(false)); } public componentWillUnmount(): void { this._unlisten(); } - public componentDidUpdate(): void { - this._overrideOnboardingStateIfShould(); + public componentDidUpdate(prevProps: PortalOnboardingFlowProps): void { + this._adjustStepIfShould(); + if (!prevProps.isRunning && this.props.isRunning) { + // On mobile, make sure the wallet is completely visible. + if (this.props.screenWidth === ScreenWidths.Sm) { + document.querySelector('.wallet').scrollIntoView(); + } + } } public render(): React.ReactNode { return ( @@ -90,45 +102,63 @@ class PlainPortalOnboardingFlow extends React.Component<PortalOnboardingFlowProp continueButtonDisplay: 'enabled', }, { - target: '.eth-row', - title: 'Add ETH', - content: <AddEthOnboardingStep />, + target: '.wallet', + title: 'Step 1: Add ETH', + content: ( + <AddEthOnboardingStep userEthBalanceInWei={this.props.userEtherBalanceInWei || new BigNumber(0)} /> + ), placement: 'right', continueButtonDisplay: this._userHasVisibleEth() ? 'enabled' : 'disabled', }, { - target: '.weth-row', - title: 'Step 1/2', + target: '.wallet', + title: 'Step 2: Wrap ETH', + content: <WrapEthOnboardingStep1 />, + placement: 'right', + continueButtonDisplay: 'enabled', + }, + { + target: '.wallet', + title: 'Step 2: Wrap ETH', + content: <WrapEthOnboardingStep2 />, + placement: 'right', + continueButtonDisplay: this._userHasVisibleWeth() ? 'enabled' : 'disabled', + }, + { + target: '.wallet', + title: 'Step 2: Wrap ETH', content: ( - <WrapEthOnboardingStep - formattedEthBalanceIfExists={ + <WrapEthOnboardingStep3 + formattedWethBalanceIfExists={ this._userHasVisibleWeth() ? this._getFormattedWethBalance() : undefined } /> ), placement: 'right', - continueButtonDisplay: this._userHasVisibleWeth() ? 'enabled' : undefined, + continueButtonDisplay: this._userHasVisibleWeth() ? 'enabled' : 'disabled', }, { - target: '.weth-row', - title: 'Step 2/2', + target: '.wallet', + title: 'Step 3: Unlock Tokens', content: ( <SetAllowancesOnboardingStep zrxAllowanceToggle={this._renderZrxAllowanceToggle()} ethAllowanceToggle={this._renderEthAllowanceToggle()} + doesUserHaveAllowancesForWethAndZrx={this._doesUserHaveAllowancesForWethAndZrx()} /> ), placement: 'right', - continueButtonDisplay: this._userHasAllowancesForWethAndZrx() ? 'enabled' : 'disabled', + continueButtonDisplay: this._doesUserHaveAllowancesForWethAndZrx() ? 'enabled' : 'disabled', }, { target: '.wallet', - title: '🎉 Congrats! The ecosystem awaits.', + title: '🎉 The Ecosystem Awaits', content: <CongratsOnboardingStep />, placement: 'right', continueButtonDisplay: 'enabled', shouldHideNextButton: true, continueButtonText: 'Enter the 0x Ecosystem', + onContinueButtonClick: this._handleFinalStepContinueClick.bind(this), }, ]; return steps; @@ -155,21 +185,18 @@ class PlainPortalOnboardingFlow extends React.Component<PortalOnboardingFlowProp private _userHasVisibleWeth(): boolean { return this._getWethBalance() > new BigNumber(0); } - private _userHasAllowancesForWethAndZrx(): boolean { + private _doesUserHaveAllowancesForWethAndZrx(): boolean { const ethToken = utils.getEthToken(this.props.tokenByAddress); const zrxToken = utils.getZrxToken(this.props.tokenByAddress); if (ethToken && zrxToken) { - const ethTokenAllowance = this.props.trackedTokenStateByAddress[ethToken.address].allowance; - const zrxTokenAllowance = this.props.trackedTokenStateByAddress[zrxToken.address].allowance; - return ethTokenAllowance > new BigNumber(0) && zrxTokenAllowance > new BigNumber(0); + const ethTokenState = this.props.trackedTokenStateByAddress[ethToken.address]; + const zrxTokenState = this.props.trackedTokenStateByAddress[zrxToken.address]; + if (ethTokenState && zrxTokenState) { + return ethTokenState.allowance.gt(0) && zrxTokenState.allowance.gt(0); + } } return false; } - private _overrideOnboardingStateIfShould(): void { - this._autoStartOnboardingIfShould(); - this._adjustStepIfShould(); - } - private _adjustStepIfShould(): void { const stepIndex = this.props.stepIndex; if (this._isAddressAvailable()) { @@ -193,7 +220,10 @@ class PlainPortalOnboardingFlow extends React.Component<PortalOnboardingFlowProp } } private _autoStartOnboardingIfShould(): void { - if (!this.props.isRunning && !this.props.hasBeenSeen && this.props.blockchainIsLoaded) { + if ( + (this.props.stepIndex === 0 && !this.props.isRunning) || + (!this.props.isRunning && !this.props.hasBeenClosed && this.props.blockchainIsLoaded) + ) { const networkName = sharedConstants.NETWORK_NAME_BY_ID[this.props.networkId]; analytics.logEvent('Portal', 'Onboarding Started - Automatic', networkName, this.props.stepIndex); this.props.updateIsRunning(true); @@ -221,18 +251,28 @@ class PlainPortalOnboardingFlow extends React.Component<PortalOnboardingFlowProp if (!token) { return null; } - const tokenState = this.props.trackedTokenStateByAddress[token.address]; + const tokenStateIfExists = this.props.trackedTokenStateByAddress[token.address]; + if (_.isUndefined(tokenStateIfExists)) { + return null; + } return ( <AllowanceToggle token={token} - tokenState={tokenState} - isDisabled={!tokenState.isLoaded} + tokenState={tokenStateIfExists} + isDisabled={!tokenStateIfExists.isLoaded} blockchain={this.props.blockchain} // tslint:disable-next-line:jsx-no-lambda refetchTokenStateAsync={async () => this.props.refetchTokenStateAsync(token.address)} /> ); } + private _handleFinalStepContinueClick(): void { + if (utils.isMobile(this.props.screenWidth)) { + window.scrollTo(0, 0); + this.props.history.push('/portal'); + } + this._closeOnboarding(); + } } export const PortalOnboardingFlow = withRouter(PlainPortalOnboardingFlow); diff --git a/packages/website/ts/components/onboarding/set_allowances_onboarding_step.tsx b/packages/website/ts/components/onboarding/set_allowances_onboarding_step.tsx index 1ff248c40..5ddfe38d7 100644 --- a/packages/website/ts/components/onboarding/set_allowances_onboarding_step.tsx +++ b/packages/website/ts/components/onboarding/set_allowances_onboarding_step.tsx @@ -5,11 +5,13 @@ import { Text } from 'ts/components/ui/text'; export interface SetAllowancesOnboardingStepProps { zrxAllowanceToggle: React.ReactNode; ethAllowanceToggle: React.ReactNode; + doesUserHaveAllowancesForWethAndZrx: boolean; } export const SetAllowancesOnboardingStep: React.StatelessComponent<SetAllowancesOnboardingStepProps> = ({ ethAllowanceToggle, zrxAllowanceToggle, + doesUserHaveAllowancesForWethAndZrx, }) => ( <div className="flex items-center flex-column"> <Text>Unlock your tokens for trading. You only need to do this once for each token.</Text> @@ -23,5 +25,6 @@ export const SetAllowancesOnboardingStep: React.StatelessComponent<SetAllowances <Container marginTop="10px">{zrxAllowanceToggle}</Container> </div> </Container> + {doesUserHaveAllowancesForWethAndZrx && <Text>Perfect! Both your ZRX and WETH tokens are unlocked.</Text>} </div> ); diff --git a/packages/website/ts/components/onboarding/wrap_eth_onboarding_step.tsx b/packages/website/ts/components/onboarding/wrap_eth_onboarding_step.tsx index b21b39341..4d336c80f 100644 --- a/packages/website/ts/components/onboarding/wrap_eth_onboarding_step.tsx +++ b/packages/website/ts/components/onboarding/wrap_eth_onboarding_step.tsx @@ -4,70 +4,78 @@ import { Container } from 'ts/components/ui/container'; import { IconButton } from 'ts/components/ui/icon_button'; import { Text } from 'ts/components/ui/text'; -export interface WrapEthOnboardingStepProps { - formattedEthBalanceIfExists?: string; +export interface WrapEthOnboardingStep1Props {} + +export const WrapEthOnboardingStep1: React.StatelessComponent<WrapEthOnboardingStep1Props> = () => ( + <div className="flex items-center flex-column"> + <Text> + You need to convert some of your ETH into tradeable <b>Wrapped ETH (WETH)</b>. + </Text> + <Container width="100%" marginTop="25px" marginBottom="15px" className="flex justify-center"> + <div className="flex flex-column items-center"> + <Text fontWeight={700}> 1 ETH </Text> + <img src="/images/eth_dollar.svg" height="75px" width="75x" /> + </div> + <Container marginRight="25px" marginLeft="25px" position="relative" top="20px"> + <Text fontSize="36px">=</Text> + </Container> + <div className="flex flex-column items-center"> + <Text fontWeight={700}> 1 WETH </Text> + <img src="/images/eth_token_erc20.svg" height="75px" width="75px" /> + </div> + </Container> + <Text> + Think of it like the coin version of a paper note. It has the same value, but some machines only take coins. + </Text> + </div> +); + +export interface WrapEthOnboardingStep2Props {} + +export const WrapEthOnboardingStep2: React.StatelessComponent<WrapEthOnboardingStep2Props> = () => ( + <div className="flex items-center flex-column"> + <Text>Wrapping your ETH is a reversable transaction, so don't worry about losing your ETH.</Text> + <Text> + Click + <Container display="inline-block" marginLeft="10px" marginRight="10px"> + <IconButton + iconName="zmdi-long-arrow-down" + color={colors.mediumBlue} + labelText="wrap" + display="inline-flex" + /> + </Container> + to wrap your ETH. + </Text> + </div> +); + +export interface WrapEthOnboardingStep3Props { + formattedWethBalanceIfExists?: string; } -export const WrapEthOnboardingStep: React.StatelessComponent<WrapEthOnboardingStepProps> = ({ - formattedEthBalanceIfExists, -}) => { - if (formattedEthBalanceIfExists) { - return ( - <div className="flex items-center flex-column"> - <Text>Congrats you now have {formattedEthBalanceIfExists} in your wallet.</Text> - <Container width="100%" marginTop="25px" marginBottom="15px" className="flex justify-center"> - <div className="flex flex-column items-center"> - <Text fontWeight={700}> 1 ETH </Text> - <img src="/images/eth_dollar.svg" height="75px" width="75x" /> - </div> - <Container marginRight="25px" marginLeft="25px" position="relative" top="20px"> - <Text fontSize="25px"> - <i className="zmdi zmdi-long-arrow-right" /> - </Text> - </Container> - <div className="flex flex-column items-center"> - <Text fontWeight={700}> 1 WETH </Text> - <img src="/images/eth_token_erc20.svg" height="75px" width="75px" /> - </div> - </Container> +export const WrapEthOnboardingStep3: React.StatelessComponent<WrapEthOnboardingStep3Props> = ({ + formattedWethBalanceIfExists, +}) => ( + <div className="flex items-center flex-column"> + <Text> + You have <b>{formattedWethBalanceIfExists || '0 WETH'}</b> in your wallet. + {formattedWethBalanceIfExists && ' Great!'} + </Text> + <Container width="100%" marginTop="25px" marginBottom="15px" className="flex justify-center"> + <div className="flex flex-column items-center"> + <Text fontWeight={700}> 1 ETH </Text> + <img src="/images/eth_dollar.svg" height="75px" width="75x" /> </div> - ); - } else { - return ( - <div className="flex items-center flex-column"> - <Text> - You need to convert some of your ETH into tradeable <b>Wrapped ETH (WETH)</b>. - </Text> - <Container width="100%" marginTop="25px" marginBottom="15px" className="flex justify-center"> - <div className="flex flex-column items-center"> - <Text fontWeight={700}> 1 ETH </Text> - <img src="/images/eth_dollar.svg" height="75px" width="75x" /> - </div> - <Container marginRight="25px" marginLeft="25px" position="relative" top="20px"> - <Text fontSize="36px">=</Text> - </Container> - <div className="flex flex-column items-center"> - <Text fontWeight={700}> 1 WETH </Text> - <img src="/images/eth_token_erc20.svg" height="75px" width="75px" /> - </div> - </Container> - <Text> - Think of it like the coin version of a paper note. It has the same value, but some machines only - take coins. - </Text> - <Text> - Click - <Container display="inline-block" marginLeft="10px" marginRight="10px"> - <IconButton - iconName="zmdi-long-arrow-down" - color={colors.mediumBlue} - labelText="wrap" - display="inline-flex" - /> - </Container> - to wrap your ETH. + <Container marginRight="25px" marginLeft="25px" position="relative" top="20px"> + <Text fontSize="25px"> + <i className="zmdi zmdi-long-arrow-right" /> </Text> + </Container> + <div className="flex flex-column items-center"> + <Text fontWeight={700}> 1 WETH </Text> + <img src="/images/eth_token_erc20.svg" height="75px" width="75px" /> </div> - ); - } -}; + </Container> + </div> +); diff --git a/packages/website/ts/components/portal/drawer_menu.tsx b/packages/website/ts/components/portal/drawer_menu.tsx index 205a60afc..a6707e86c 100644 --- a/packages/website/ts/components/portal/drawer_menu.tsx +++ b/packages/website/ts/components/portal/drawer_menu.tsx @@ -44,12 +44,13 @@ export const DrawerMenu = (props: DrawerMenuProps) => { iconName: 'zmdi-portable-wifi', }; const menuItemEntries = _.concat(relayerItemEntry, defaultMenuItemEntries); - const displayMessage = utils.getReadableAccountState( + const accountState = utils.getAccountState( props.blockchainIsLoaded && !_.isUndefined(props.blockchain), props.providerType, props.injectedProviderName, props.userAddress, ); + const displayMessage = utils.getReadableAccountState(accountState, props.userAddress); return ( <div style={styles.root}> <Header userAddress={props.userAddress} displayMessage={displayMessage} /> diff --git a/packages/website/ts/components/portal/portal.tsx b/packages/website/ts/components/portal/portal.tsx index 4166fde53..9c0cb866d 100644 --- a/packages/website/ts/components/portal/portal.tsx +++ b/packages/website/ts/components/portal/portal.tsx @@ -1,10 +1,10 @@ import { colors, constants as sharedConstants } from '@0xproject/react-shared'; import { BigNumber } from '@0xproject/utils'; import * as _ from 'lodash'; -import ActionAccountBalanceWallet from 'material-ui/svg-icons/action/account-balance-wallet'; +import Help from 'material-ui/svg-icons/action/help'; import * as React from 'react'; import * as DocumentTitle from 'react-document-title'; -import { Route, RouteComponentProps, Switch } from 'react-router-dom'; +import { Link, Route, RouteComponentProps, Switch } from 'react-router-dom'; import { Blockchain } from 'ts/blockchain'; import { BlockchainErrDialog } from 'ts/components/dialogs/blockchain_err_dialog'; @@ -24,7 +24,6 @@ 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 { Island } from 'ts/components/ui/island'; import { Text } from 'ts/components/ui/text'; import { Wallet } from 'ts/components/wallet/wallet'; import { GenerateOrderForm } from 'ts/containers/generate_order_form'; @@ -152,9 +151,8 @@ export class Portal extends React.Component<PortalProps, PortalState> { } public componentDidUpdate(prevProps: PortalProps): void { if (!prevProps.blockchainIsLoaded && this.props.blockchainIsLoaded) { - const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress); // tslint:disable-next-line:no-floating-promises - this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses); + this._fetchBalancesAndAllowancesAsync(this._getCurrentTrackedTokensAddresses()); } } public componentWillReceiveProps(nextProps: PortalProps): void { @@ -182,17 +180,17 @@ export class Portal extends React.Component<PortalProps, PortalState> { 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.userAddress !== this.props.userAddress && nextProps.networkId === this.props.networkId) || nextProps.lastForceTokenStateRefetch !== this.props.lastForceTokenStateRefetch ) { - const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress); // tslint:disable-next-line:no-floating-promises - this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses); + this._fetchBalancesAndAllowancesAsync(this._getCurrentTrackedTokensAddresses()); } - const nextTrackedTokens = this._getTrackedTokens(nextProps.tokenByAddress); + const nextTrackedTokens = utils.getTrackedTokens(nextProps.tokenByAddress); const trackedTokens = this._getCurrentTrackedTokens(); if (!_.isEqual(nextTrackedTokens, trackedTokens)) { @@ -200,7 +198,7 @@ export class Portal extends React.Component<PortalProps, PortalState> { 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; + const trackedTokenStateByAddress = { ...this.state.trackedTokenStateByAddress }; for (const tokenAddress of newTokenAddresses) { trackedTokenStateByAddress[tokenAddress] = { balance: new BigNumber(0), @@ -265,16 +263,16 @@ export class Portal extends React.Component<PortalProps, PortalState> { networkId={this.props.networkId} /> <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} - /> - )} + + <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} @@ -320,15 +318,14 @@ export class Portal extends React.Component<PortalProps, PortalState> { ); } private _renderWallet(): React.ReactNode { - const startOnboarding = this._renderStartOnboarding(); const isMobile = utils.isMobile(this.props.screenWidth); // We need room to scroll down for mobile onboarding const marginBottom = isMobile ? '200px' : '15px'; return ( <div> - <Container> - {isMobile && <Container marginBottom="15px">{startOnboarding}</Container>} - <Container marginBottom={marginBottom}> + <Container className="flex flex-column items-center"> + {isMobile && <Container marginBottom="20px">{this._renderStartOnboarding()}</Container>} + <Container marginBottom={marginBottom} width="100%"> <Wallet style={ !isMobile && this.props.isPortalOnboardingShowing @@ -356,7 +353,7 @@ export class Portal extends React.Component<PortalProps, PortalState> { refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this)} /> </Container> - {!isMobile && <Container marginTop="15px">{startOnboarding}</Container>} + {!isMobile && <Container marginTop="8px">{this._renderStartOnboarding()}</Container>} </Container> <PortalOnboardingFlow blockchain={this._blockchain} @@ -367,26 +364,24 @@ export class Portal extends React.Component<PortalProps, PortalState> { ); } private _renderStartOnboarding(): React.ReactNode { - return ( - <Island> - <Container - marginTop="30px" - marginBottom="30px" - marginLeft="30px" - marginRight="30px" - className="flex justify-around items-center" - > - <ActionAccountBalanceWallet style={{ width: '30px', height: '30px' }} color={colors.orange} /> - <Text - fontColor={colors.grey} - fontSize="16px" - center={true} - onClick={this._startOnboarding.bind(this)} - > + const isMobile = utils.isMobile(this.props.screenWidth); + const shouldStartOnboarding = !isMobile || this.props.location.pathname === `${WebsitePaths.Portal}/account`; + const startOnboarding = ( + <Container className="flex items-center center"> + <Help style={{ width: '20px', height: '20px' }} color={colors.mediumBlue} /> + <Container marginLeft="8px"> + <Text fontColor={colors.mediumBlue} fontSize="16px" onClick={this._startOnboarding.bind(this)}> Learn how to set up your account </Text> </Container> - </Island> + </Container> + ); + return !shouldStartOnboarding ? ( + <Link to={{ pathname: `${WebsitePaths.Portal}/account` }} style={{ textDecoration: 'none' }}> + {startOnboarding} + </Link> + ) : ( + startOnboarding ); } @@ -394,10 +389,6 @@ export class Portal extends React.Component<PortalProps, PortalState> { const networkName = sharedConstants.NETWORK_NAME_BY_ID[this.props.networkId]; analytics.logEvent('Portal', 'Onboarding Started - Manual', networkName, this.props.portalOnboardingStep); this.props.dispatcher.updatePortalOnboardingShowing(true); - // On mobile, make sure the wallet is completely visible. - if (this.props.screenWidth === ScreenWidths.Sm) { - document.querySelector('.wallet').scrollIntoView(); - } } private _renderWalletSection(): React.ReactNode { return <Section header={<TextHeader labelText="Your Account" />} body={this._renderWallet()} />; @@ -536,11 +527,15 @@ export class Portal extends React.Component<PortalProps, PortalState> { ); } private _renderRelayerIndexSection(): React.ReactNode { + return <Section header={<TextHeader labelText="0x Relayers" />} body={this._renderRelayerIndex()} />; + } + private _renderRelayerIndex(): React.ReactNode { + const isMobile = utils.isMobile(this.props.screenWidth); return ( - <Section - header={<TextHeader labelText="0x Relayers" />} - body={<RelayerIndex networkId={this.props.networkId} screenWidth={this.props.screenWidth} />} - /> + <Container className="flex flex-column items-center"> + {isMobile && <Container marginBottom="20px">{this._renderStartOnboarding()}</Container>} + <RelayerIndex networkId={this.props.networkId} screenWidth={this.props.screenWidth} /> + </Container> ); } private _renderNotFoundMessage(): React.ReactNode { @@ -563,9 +558,9 @@ export class Portal extends React.Component<PortalProps, PortalState> { if (this.state.tokenManagementState === TokenManagementState.Remove && !isDefaultTrackedToken) { if (token.isRegistered) { // Remove the token from tracked tokens - const newToken = { + const newToken: Token = { ...token, - isTracked: false, + trackedTimestamp: undefined, }; this.props.dispatcher.updateTokenByAddress([newToken]); } else { @@ -608,17 +603,12 @@ export class Portal extends React.Component<PortalProps, PortalState> { const isSmallScreen = this.props.screenWidth === ScreenWidths.Sm; return isSmallScreen; } - private _getCurrentTrackedTokens(): Token[] { - return this._getTrackedTokens(this.props.tokenByAddress); + return utils.getTrackedTokens(this.props.tokenByAddress); } - - private _getTrackedTokens(tokenByAddress: TokenByAddress): Token[] { - const allTokens = _.values(tokenByAddress); - const trackedTokens = _.filter(allTokens, t => t.isTracked); - return trackedTokens; + private _getCurrentTrackedTokensAddresses(): string[] { + return _.map(this._getCurrentTrackedTokens(), token => token.address); } - private _getInitialTrackedTokenStateByAddress(trackedTokens: Token[]): TokenStateByAddress { const trackedTokenStateByAddress: TokenStateByAddress = {}; _.each(trackedTokens, token => { diff --git a/packages/website/ts/components/portal/section.tsx b/packages/website/ts/components/portal/section.tsx index 455ed07c9..b6c9fd098 100644 --- a/packages/website/ts/components/portal/section.tsx +++ b/packages/website/ts/components/portal/section.tsx @@ -6,9 +6,9 @@ export interface SectionProps { } export const Section = (props: SectionProps) => { return ( - <div className="flex flex-column" style={{ height: '100%' }}> + <div className="flex flex-column"> {props.header} - <div className="flex-auto">{props.body}</div> + {props.body} </div> ); }; diff --git a/packages/website/ts/components/relayer_index/relayer_grid_tile.tsx b/packages/website/ts/components/relayer_index/relayer_grid_tile.tsx index 18b069ae2..b26bf512b 100644 --- a/packages/website/ts/components/relayer_index/relayer_grid_tile.tsx +++ b/packages/website/ts/components/relayer_index/relayer_grid_tile.tsx @@ -54,7 +54,7 @@ const styles: Styles = { }, }; -const FALLBACK_IMG_SRC = '/images/landing/hero_chip_image.png'; +const FALLBACK_IMG_SRC = '/images/relayer_fallback.png'; const FALLBACK_PRIMARY_COLOR = colors.grey200; const NO_CONTENT_MESSAGE = '--'; const RELAYER_ICON_HEIGHT = '110px'; diff --git a/packages/website/ts/components/relayer_index/relayer_top_tokens.tsx b/packages/website/ts/components/relayer_index/relayer_top_tokens.tsx index f544fc924..c48b672e9 100644 --- a/packages/website/ts/components/relayer_index/relayer_top_tokens.tsx +++ b/packages/website/ts/components/relayer_index/relayer_top_tokens.tsx @@ -2,44 +2,30 @@ import { colors, constants as sharedConstants, EtherscanLinkSuffixes, - Styles, utils as sharedUtils, } from '@0xproject/react-shared'; import * as _ from 'lodash'; import * as React from 'react'; -import { analytics } from 'ts/utils/analytics'; +import { Container } from 'ts/components/ui/container'; +import { Text } from 'ts/components/ui/text'; import { WebsiteBackendTokenInfo } from 'ts/types'; +import { analytics } from 'ts/utils/analytics'; +import { utils } from 'ts/utils/utils'; export interface TopTokensProps { tokens: WebsiteBackendTokenInfo[]; networkId: number; } -const styles: Styles = { - tokenLabel: { - textDecoration: 'none', - color: colors.mediumBlue, - fontSize: 14, - }, - followingTokenLabel: { - paddingLeft: 16, - }, -}; - export const TopTokens: React.StatelessComponent<TopTokensProps> = (props: TopTokensProps) => { return ( <div className="flex"> - {_.map(props.tokens, (tokenInfo: WebsiteBackendTokenInfo, index: number) => { - const firstItemStyle = { ...styles.tokenLabel, ...styles.followingTokenLabel }; - const style = index !== 0 ? firstItemStyle : styles.tokenLabel; + {_.map(props.tokens, (tokenInfo: WebsiteBackendTokenInfo) => { return ( - <TokenLink - key={tokenInfo.address} - tokenInfo={tokenInfo} - style={style} - networkId={props.networkId} - /> + <Container key={tokenInfo.address} marginRight="16px"> + <TokenLink tokenInfo={tokenInfo} networkId={props.networkId} /> + </Container> ); })} </div> @@ -48,12 +34,9 @@ export const TopTokens: React.StatelessComponent<TopTokensProps> = (props: TopTo interface TokenLinkProps { tokenInfo: WebsiteBackendTokenInfo; - style: React.CSSProperties; networkId: number; } -interface TokenLinkState { - isHovering: boolean; -} +interface TokenLinkState {} class TokenLink extends React.Component<TokenLinkProps, TokenLinkState> { constructor(props: TokenLinkProps) { @@ -63,37 +46,21 @@ class TokenLink extends React.Component<TokenLinkProps, TokenLinkState> { }; } public render(): React.ReactNode { - const style = { - ...this.props.style, - cursor: 'pointer', - opacity: this.state.isHovering ? 0.5 : 1, - }; const networkName = sharedConstants.NETWORK_NAME_BY_ID[this.props.networkId]; const eventLabel = `${this.props.tokenInfo.symbol}-${networkName}`; const onClick = (event: React.MouseEvent<HTMLElement>) => { event.stopPropagation(); analytics.logEvent('Portal', 'Token Click', eventLabel); + const tokenLink = this._tokenLinkFromToken(this.props.tokenInfo, this.props.networkId); + utils.openUrl(tokenLink); }; return ( - <a - href={tokenLinkFromToken(this.props.tokenInfo, this.props.networkId)} - target="_blank" - style={style} - onMouseEnter={this._onToggleHover.bind(this, true)} - onMouseLeave={this._onToggleHover.bind(this, false)} - onClick={onClick} - > + <Text fontSize="14px" fontColor={colors.mediumBlue} onClick={onClick}> {this.props.tokenInfo.symbol} - </a> + </Text> ); } - private _onToggleHover(isHovering: boolean): void { - this.setState({ - isHovering, - }); + private _tokenLinkFromToken(tokenInfo: WebsiteBackendTokenInfo, networkId: number): string { + return sharedUtils.getEtherScanLinkIfExists(tokenInfo.address, networkId, EtherscanLinkSuffixes.Address); } } - -function tokenLinkFromToken(tokenInfo: WebsiteBackendTokenInfo, networkId: number): string { - return sharedUtils.getEtherScanLinkIfExists(tokenInfo.address, networkId, EtherscanLinkSuffixes.Address); -} diff --git a/packages/website/ts/components/token_balances.tsx b/packages/website/ts/components/token_balances.tsx index 7af80745c..3fae83c00 100644 --- a/packages/website/ts/components/token_balances.tsx +++ b/packages/website/ts/components/token_balances.tsx @@ -308,7 +308,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala const trackedTokensStartingWithEtherToken = trackedTokens.sort( firstBy((t: Token) => t.symbol !== ETHER_TOKEN_SYMBOL) .thenBy((t: Token) => t.symbol !== ZRX_TOKEN_SYMBOL) - .thenBy('address'), + .thenBy('trackedTimestamp'), ); const tableRows = _.map( trackedTokensStartingWithEtherToken, @@ -424,9 +424,9 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala if (!this.state.isAddingToken && !isDefaultTrackedToken) { if (token.isRegistered) { // Remove the token from tracked tokens - const newToken = { + const newToken: Token = { ...token, - isTracked: false, + trackedTimestamp: undefined, }; this.props.dispatcher.updateTokenByAddress([newToken]); } else { diff --git a/packages/website/ts/components/top_bar/provider_display.tsx b/packages/website/ts/components/top_bar/provider_display.tsx index 496e5cae0..8743e4320 100644 --- a/packages/website/ts/components/top_bar/provider_display.tsx +++ b/packages/website/ts/components/top_bar/provider_display.tsx @@ -2,19 +2,21 @@ import { Styles } from '@0xproject/react-shared'; import * as _ from 'lodash'; import CircularProgress from 'material-ui/CircularProgress'; import RaisedButton from 'material-ui/RaisedButton'; +import ActionAccountBalanceWallet from 'material-ui/svg-icons/action/account-balance-wallet'; +import Lock from 'material-ui/svg-icons/action/lock'; import * as React from 'react'; import { Blockchain } from 'ts/blockchain'; import { ProviderPicker } from 'ts/components/top_bar/provider_picker'; +import { AccountConnection } from 'ts/components/ui/account_connection'; import { Container } from 'ts/components/ui/container'; import { DropDown } from 'ts/components/ui/drop_down'; import { Identicon } from 'ts/components/ui/identicon'; -import { Image } from 'ts/components/ui/image'; import { Island } from 'ts/components/ui/island'; import { Text } from 'ts/components/ui/text'; import { Dispatcher } from 'ts/redux/dispatcher'; import { colors } from 'ts/style/colors'; -import { ProviderType } from 'ts/types'; +import { AccountState, ProviderType } from 'ts/types'; import { constants } from 'ts/utils/constants'; import { utils } from 'ts/utils/utils'; @@ -46,37 +48,13 @@ export class ProviderDisplay extends React.Component<ProviderDisplayProps, Provi this.props.providerType, this.props.injectedProviderName, ); - const displayMessage = utils.getReadableAccountState( - this._isBlockchainReady(), - this.props.providerType, - this.props.injectedProviderName, - this.props.userAddress, - ); - // 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 isProviderMetamask = providerTitle === constants.PROVIDER_NAME_METAMASK; const hoverActiveNode = ( - <Island className="flex items-center p1" style={styles.root}> - <div> - {this._isBlockchainReady() ? ( - <Identicon address={this.props.userAddress} diameter={ROOT_HEIGHT} /> - ) : ( - <CircularProgress size={ROOT_HEIGHT} thickness={2} /> - )} - </div> + <Island className="flex items-center py1 px2" style={styles.root}> + {this._renderIcon()} <Container marginLeft="12px" marginRight="12px"> - <Text fontSize="14px" fontColor={colors.darkGrey}> - {displayMessage} - </Text> + {this._renderDisplayMessage()} </Container> - {isProviderMetamask && ( - <Image src="/images/metamask_icon.png" height={ROOT_HEIGHT} width={ROOT_HEIGHT} /> - )} + {this._renderInjectedProvider()} </Island> ); const hasLedgerProvider = this.props.providerType === ProviderType.Ledger; @@ -168,7 +146,69 @@ export class ProviderDisplay extends React.Component<ProviderDisplayProps, Provi ); } } + private _renderIcon(): React.ReactNode { + const accountState = this._getAccountState(); + switch (accountState) { + case AccountState.Ready: + return <Identicon address={this.props.userAddress} diameter={ROOT_HEIGHT} />; + case AccountState.Loading: + return <CircularProgress size={ROOT_HEIGHT} thickness={2} />; + case AccountState.Locked: + return <Lock color={colors.black} />; + case AccountState.Disconnected: + return <ActionAccountBalanceWallet color={colors.mediumBlue} />; + default: + return null; + } + } + private _renderDisplayMessage(): React.ReactNode { + const accountState = this._getAccountState(); + const displayMessage = utils.getReadableAccountState(accountState, this.props.userAddress); + const fontColor = this._getDisplayMessageFontColor(); + return ( + <Text fontSize="16px" fontColor={fontColor} fontWeight={500}> + {displayMessage} + </Text> + ); + } + private _getDisplayMessageFontColor(): string { + const accountState = this._getAccountState(); + switch (accountState) { + case AccountState.Loading: + return colors.darkGrey; + case AccountState.Ready: + case AccountState.Locked: + case AccountState.Disconnected: + default: + return colors.black; + } + } + private _renderInjectedProvider(): React.ReactNode { + const accountState = this._getAccountState(); + switch (accountState) { + case AccountState.Ready: + case AccountState.Locked: + return ( + <AccountConnection + accountState={accountState} + injectedProviderName={this.props.injectedProviderName} + /> + ); + case AccountState.Disconnected: + case AccountState.Loading: + default: + return null; + } + } private _isBlockchainReady(): boolean { return this.props.blockchainIsLoaded && !_.isUndefined(this.props.blockchain); } + private _getAccountState(): AccountState { + return utils.getAccountState( + this._isBlockchainReady(), + this.props.providerType, + this.props.injectedProviderName, + this.props.userAddress, + ); + } } diff --git a/packages/website/ts/components/top_bar/top_bar.tsx b/packages/website/ts/components/top_bar/top_bar.tsx index 537edc7bb..fac6c131f 100644 --- a/packages/website/ts/components/top_bar/top_bar.tsx +++ b/packages/website/ts/components/top_bar/top_bar.tsx @@ -13,7 +13,6 @@ 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 { Dispatcher } from 'ts/redux/dispatcher'; -import { zIndex } from 'ts/style/z_index'; import { Deco, Key, ProviderType, WebsiteLegacyPaths, WebsitePaths } from 'ts/types'; import { constants } from 'ts/utils/constants'; import { Translate } from 'ts/utils/translate'; @@ -58,7 +57,6 @@ const styles: Styles = { width: '100%', position: 'relative', top: 0, - zIndex: zIndex.topBar, paddingBottom: 1, }, bottomBar: { diff --git a/packages/website/ts/components/ui/account_connection.tsx b/packages/website/ts/components/ui/account_connection.tsx new file mode 100644 index 000000000..6d0b90922 --- /dev/null +++ b/packages/website/ts/components/ui/account_connection.tsx @@ -0,0 +1,40 @@ +import * as React from 'react'; + +import { Circle } from 'ts/components/ui/circle'; +import { Container } from 'ts/components/ui/container'; +import { Text } from 'ts/components/ui/text'; +import { colors } from 'ts/style/colors'; +import { AccountState } from 'ts/types'; + +export interface AccountConnectionProps { + accountState: AccountState; + injectedProviderName: string; +} + +export const AccountConnection: React.StatelessComponent<AccountConnectionProps> = ({ + accountState, + injectedProviderName, +}) => { + return ( + <Container className="flex items-center"> + <Circle diameter={6} fillColor={getInjectedProviderColor(accountState)} /> + <Container marginLeft="6px"> + <Text fontSize="12px" lineHeight="14px" fontColor={colors.darkGrey}> + {injectedProviderName} + </Text> + </Container> + </Container> + ); +}; + +const getInjectedProviderColor = (accountState: AccountState) => { + switch (accountState) { + case AccountState.Ready: + return colors.limeGreen; + case AccountState.Locked: + case AccountState.Loading: + case AccountState.Disconnected: + default: + return colors.red; + } +}; diff --git a/packages/website/ts/components/ui/animation.tsx b/packages/website/ts/components/ui/animation.tsx index 136f3d005..943e3bf28 100644 --- a/packages/website/ts/components/ui/animation.tsx +++ b/packages/website/ts/components/ui/animation.tsx @@ -14,21 +14,29 @@ const appearFromBottomFrames = keyframes` position: fixed; bottom: -500px; left: 0px; + right: 0px; } to { position: fixed; bottom: 0px; left: 0px; + right: 0px; } `; +const stylesForAnimation: { [K in AnimationType]: string } = { + // Needed for safari + easeUpFromBottom: `position: fixed`, +}; + const animations: { [K in AnimationType]: string } = { easeUpFromBottom: `${appearFromBottomFrames} 1s ease 0s 1 forwards`, }; export const Animation = styled(PlainAnimation)` animation: ${props => animations[props.type]}; + ${props => stylesForAnimation[props.type]}; `; Animation.displayName = 'Animation'; diff --git a/packages/website/ts/components/ui/button.tsx b/packages/website/ts/components/ui/button.tsx index 02fa47480..1489a74a6 100644 --- a/packages/website/ts/components/ui/button.tsx +++ b/packages/website/ts/components/ui/button.tsx @@ -37,7 +37,7 @@ export const Button = styled(PlainButton)` background-color: ${props => props.backgroundColor}; border: ${props => (props.borderColor ? `1px solid ${props.borderColor}` : 'none')}; &:hover { - background-color: ${props => (!props.isDisabled ? darken(0.1, props.backgroundColor) : '')}; + background-color: ${props => (!props.isDisabled ? darken(0.1, props.backgroundColor) : '')} !important; } &:active { background-color: ${props => (!props.isDisabled ? darken(0.2, props.backgroundColor) : '')}; diff --git a/packages/website/ts/components/ui/circle.tsx b/packages/website/ts/components/ui/circle.tsx new file mode 100644 index 000000000..75103d066 --- /dev/null +++ b/packages/website/ts/components/ui/circle.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; + +export interface CircleProps { + className?: string; + diameter: number; + fillColor: string; +} + +export const Circle: React.StatelessComponent<CircleProps> = ({ className, diameter, fillColor }) => { + const radius = diameter / 2; + return ( + <svg className={className} height={diameter} width={diameter}> + <circle cx={radius} cy={radius} r={radius} fill={fillColor} /> + </svg> + ); +}; diff --git a/packages/website/ts/components/ui/container.tsx b/packages/website/ts/components/ui/container.tsx index a747ef01f..fb718d731 100644 --- a/packages/website/ts/components/ui/container.tsx +++ b/packages/website/ts/components/ui/container.tsx @@ -14,7 +14,9 @@ export interface ContainerProps { backgroundColor?: string; borderRadius?: StringOrNum; maxWidth?: StringOrNum; + maxHeight?: StringOrNum; width?: StringOrNum; + height?: StringOrNum; minHeight?: StringOrNum; isHidden?: boolean; className?: string; diff --git a/packages/website/ts/components/ui/identicon.tsx b/packages/website/ts/components/ui/identicon.tsx index cc1655962..b5b374973 100644 --- a/packages/website/ts/components/ui/identicon.tsx +++ b/packages/website/ts/components/ui/identicon.tsx @@ -2,6 +2,7 @@ import blockies = require('blockies'); import * as _ from 'lodash'; import * as React from 'react'; +import { Circle } from 'ts/components/ui/circle'; import { Image } from 'ts/components/ui/image'; import { colors } from 'ts/style/colors'; @@ -20,7 +21,6 @@ export class Identicon extends React.Component<IdenticonProps, IdenticonState> { public render(): React.ReactNode { const address = this.props.address; const diameter = this.props.diameter; - const radius = diameter / 2; return ( <div className="circle relative transitionFix" @@ -40,9 +40,7 @@ export class Identicon extends React.Component<IdenticonProps, IdenticonState> { width={diameter} /> ) : ( - <svg height={diameter} width={diameter}> - <circle cx={radius} cy={radius} r={radius} fill={colors.grey200} /> - </svg> + <Circle diameter={diameter} fillColor={colors.grey200} /> )} </div> ); diff --git a/packages/website/ts/components/ui/overlay.tsx b/packages/website/ts/components/ui/overlay.tsx index 8b126a6d5..da26317de 100644 --- a/packages/website/ts/components/ui/overlay.tsx +++ b/packages/website/ts/components/ui/overlay.tsx @@ -4,7 +4,6 @@ import * as React from 'react'; import { zIndex } from 'ts/style/z_index'; export interface OverlayProps { - children?: React.ReactNode; style?: React.CSSProperties; onClick?: () => void; } @@ -19,7 +18,7 @@ const style: React.CSSProperties = { backgroundColor: 'rgba(0, 0, 0, 0.6)', }; -export const Overlay: React.StatelessComponent = (props: OverlayProps) => ( +export const Overlay: React.StatelessComponent<OverlayProps> = props => ( <div style={{ ...style, ...props.style }} onClick={props.onClick}> {props.children} </div> diff --git a/packages/website/ts/components/ui/text.tsx b/packages/website/ts/components/ui/text.tsx index 1e2a123b7..c1cb2ade4 100644 --- a/packages/website/ts/components/ui/text.tsx +++ b/packages/website/ts/components/ui/text.tsx @@ -15,7 +15,8 @@ export interface TextProps { minHeight?: string; center?: boolean; fontWeight?: number | string; - onClick?: () => void; + textDecorationLine?: string; + onClick?: (event: React.MouseEvent<HTMLElement>) => void; } const PlainText: React.StatelessComponent<TextProps> = ({ children, className, onClick, Tag }) => ( @@ -28,6 +29,7 @@ export const Text = styled(PlainText)` font-family: ${props => props.fontFamily}; font-weight: ${props => props.fontWeight}; font-size: ${props => props.fontSize}; + text-decoration-line: ${props => props.textDecorationLine}; ${props => (props.lineHeight ? `line-height: ${props.lineHeight}` : '')}; ${props => (props.center ? 'text-align: center' : '')}; color: ${props => props.fontColor}; @@ -35,7 +37,7 @@ export const Text = styled(PlainText)` ${props => (props.onClick ? 'cursor: pointer' : '')}; transition: color 0.5s ease; &:hover { - ${props => (props.onClick ? `color: ${darken(0.1, props.fontColor)}` : '')}; + ${props => (props.onClick ? `color: ${darken(0.3, props.fontColor)}` : '')}; } `; @@ -45,6 +47,7 @@ Text.defaultProps = { fontColor: colors.black, fontSize: '15px', lineHeight: '1.5em', + textDecorationLine: 'none', Tag: 'div', }; diff --git a/packages/website/ts/components/wallet/body_overlay.tsx b/packages/website/ts/components/wallet/body_overlay.tsx new file mode 100644 index 000000000..5ced704f9 --- /dev/null +++ b/packages/website/ts/components/wallet/body_overlay.tsx @@ -0,0 +1,146 @@ +import * as _ from 'lodash'; +import * as React from 'react'; + +import { Blockchain } from 'ts/blockchain'; +import { Container } from 'ts/components/ui/container'; +import { Image } from 'ts/components/ui/image'; +import { Island } from 'ts/components/ui/island'; +import { Text } from 'ts/components/ui/text'; +import { Dispatcher } from 'ts/redux/dispatcher'; +import { colors } from 'ts/style/colors'; +import { styled } from 'ts/style/theme'; +import { AccountState, BrowserType, ProviderType } from 'ts/types'; +import { constants } from 'ts/utils/constants'; +import { utils } from 'ts/utils/utils'; + +const METAMASK_IMG_SRC = '/images/metamask_icon.png'; + +export interface BodyOverlayProps { + dispatcher: Dispatcher; + userAddress: string; + injectedProviderName: string; + providerType: ProviderType; + onToggleLedgerDialog: () => void; + blockchain?: Blockchain; + blockchainIsLoaded: boolean; +} + +interface BodyOverlayState {} + +export class BodyOverlay extends React.Component<BodyOverlayProps, BodyOverlayState> { + public render(): React.ReactNode { + const accountState = this._getAccountState(); + switch (accountState) { + case AccountState.Locked: + return <LockedOverlay onUseDifferentWalletClicked={this.props.onToggleLedgerDialog} />; + case AccountState.Disconnected: + return <DisconnectedOverlay onUseDifferentWalletClicked={this.props.onToggleLedgerDialog} />; + case AccountState.Ready: + case AccountState.Loading: + default: + return null; + } + } + private _isBlockchainReady(): boolean { + return this.props.blockchainIsLoaded && !_.isUndefined(this.props.blockchain); + } + private _getAccountState(): AccountState { + return utils.getAccountState( + this._isBlockchainReady(), + this.props.providerType, + this.props.injectedProviderName, + this.props.userAddress, + ); + } +} + +interface LockedOverlayProps { + className?: string; + onUseDifferentWalletClicked?: () => void; +} +const PlainLockedOverlay: React.StatelessComponent<LockedOverlayProps> = ({ + className, + onUseDifferentWalletClicked, +}) => ( + <div className={className}> + <Container + className="flex flex-column items-center" + marginBottom="24px" + marginTop="24px" + marginLeft="48px" + marginRight="48px" + > + <Image src={METAMASK_IMG_SRC} height="70px" /> + <Container marginTop="12px"> + <Text fontColor={colors.metaMaskOrange} fontSize="16px" fontWeight="bold"> + Please Unlock MetaMask + </Text> + </Container> + <UseDifferentWallet fontColor={colors.darkGrey} onClick={onUseDifferentWalletClicked} /> + </Container> + </div> +); +const LockedOverlay = styled(PlainLockedOverlay)` + background: ${colors.metaMaskTransparentOrange}; + border: 1px solid ${colors.metaMaskOrange}; + border-radius: 10px; +`; + +interface DisconnectedOverlayProps { + onUseDifferentWalletClicked?: () => void; +} +const DisconnectedOverlay = (props: DisconnectedOverlayProps) => { + return ( + <div className="flex flex-column items-center"> + <GetMetaMask /> + <UseDifferentWallet fontColor={colors.mediumBlue} onClick={props.onUseDifferentWalletClicked} /> + </div> + ); +}; + +interface UseDifferentWallet { + fontColor: string; + onClick?: () => void; +} +const UseDifferentWallet = (props: UseDifferentWallet) => { + return ( + <Container marginTop="12px"> + <Text fontColor={props.fontColor} fontSize="16px" textDecorationLine="underline" onClick={props.onClick}> + Use a different wallet + </Text> + </Container> + ); +}; + +const GetMetaMask = () => { + const browserType = utils.getBrowserType(); + let extensionLink; + switch (browserType) { + case BrowserType.Chrome: + extensionLink = constants.URL_METAMASK_CHROME_STORE; + break; + case BrowserType.Firefox: + extensionLink = constants.URL_METAMASK_FIREFOX_STORE; + break; + case BrowserType.Opera: + extensionLink = constants.URL_METAMASK_OPERA_STORE; + break; + default: + extensionLink = constants.URL_METAMASK_HOMEPAGE; + } + return ( + <a href={extensionLink} target="_blank" style={{ textDecoration: 'none' }}> + <Island + className="flex items-center py1 px2" + style={{ height: 28, borderRadius: 28, backgroundColor: colors.mediumBlue }} + > + <Image src={METAMASK_IMG_SRC} width="28px" /> + <Container marginLeft="8px" marginRight="12px"> + <Text fontColor={colors.white} fontSize="16px" fontWeight={500}> + Get MetaMask Wallet + </Text> + </Container> + </Island> + </a> + ); +}; diff --git a/packages/website/ts/components/wallet/null_token_row.tsx b/packages/website/ts/components/wallet/null_token_row.tsx new file mode 100644 index 000000000..a1ec9871a --- /dev/null +++ b/packages/website/ts/components/wallet/null_token_row.tsx @@ -0,0 +1,41 @@ +import * as React from 'react'; + +import { Circle } from 'ts/components/ui/circle'; +import { Container } from 'ts/components/ui/container'; +import { Text } from 'ts/components/ui/text'; +import { PlaceHolder } from 'ts/components/wallet/placeholder'; +import { StandardIconRow } from 'ts/components/wallet/standard_icon_row'; +import { colors } from 'ts/style/colors'; + +export interface NullTokenRowProps { + iconDimension: number; + fillColor: string; +} + +export const NullTokenRow: React.StatelessComponent<NullTokenRowProps> = ({ iconDimension, fillColor }) => { + const icon = <Circle diameter={iconDimension} fillColor={fillColor} />; + const main = ( + <div className="flex flex-column"> + <PlaceHolder hideChildren={true} fillColor={fillColor}> + <Text fontSize="16px" fontWeight="bold" lineHeight="1em"> + 0.00 XXX + </Text> + </PlaceHolder> + <Container marginTop="3px"> + <PlaceHolder hideChildren={true} fillColor={fillColor}> + <Text fontSize="14px" fontColor={colors.darkGrey} lineHeight="1em"> + $0.00 + </Text> + </PlaceHolder> + </Container> + </div> + ); + const accessory = ( + <Container marginRight="12px"> + <PlaceHolder hideChildren={true} fillColor={fillColor}> + <Container width="20px" height="14px" /> + </PlaceHolder> + </Container> + ); + return <StandardIconRow icon={icon} main={main} accessory={accessory} />; +}; diff --git a/packages/website/ts/components/wallet/placeholder.tsx b/packages/website/ts/components/wallet/placeholder.tsx new file mode 100644 index 000000000..bf40d2ea8 --- /dev/null +++ b/packages/website/ts/components/wallet/placeholder.tsx @@ -0,0 +1,25 @@ +import * as React from 'react'; + +import { styled } from 'ts/style/theme'; + +export interface PlaceHolderProps { + className?: string; + hideChildren: React.ReactNode; + fillColor: string; +} + +const PlainPlaceHolder: React.StatelessComponent<PlaceHolderProps> = ({ className, hideChildren, children }) => { + const childrenVisibility = hideChildren ? 'hidden' : 'visible'; + const childrenStyle: React.CSSProperties = { visibility: childrenVisibility }; + return ( + <div className={className}> + <div style={childrenStyle}>{children}</div> + </div> + ); +}; + +export const PlaceHolder = styled(PlainPlaceHolder)` + background-color: ${props => (props.hideChildren ? props.fillColor : 'transparent')}; + display: inline-block; + border-radius: 2px; +`; diff --git a/packages/website/ts/components/wallet/standard_icon_row.tsx b/packages/website/ts/components/wallet/standard_icon_row.tsx new file mode 100644 index 000000000..1a2ec021b --- /dev/null +++ b/packages/website/ts/components/wallet/standard_icon_row.tsx @@ -0,0 +1,44 @@ +import * as React from 'react'; + +import { colors } from 'ts/style/colors'; +import { styled } from 'ts/style/theme'; + +export interface StandardIconRowProps { + className?: string; + icon: React.ReactNode; + main: React.ReactNode; + accessory?: React.ReactNode; + minHeight?: string; + borderBottomColor?: string; + borderBottomStyle?: string; + borderWidth?: string; + backgroundColor?: string; +} +const PlainStandardIconRow: React.StatelessComponent<StandardIconRowProps> = ({ className, icon, main, accessory }) => { + return ( + <div className={`flex items-center ${className}`}> + <div className="flex items-center px2">{icon}</div> + <div className="flex-none pr2">{main}</div> + <div className="flex-auto" /> + <div>{accessory}</div> + </div> + ); +}; + +export const StandardIconRow = styled(PlainStandardIconRow)` + min-height: ${props => props.minHeight}; + border-bottom-color: ${props => props.borderBottomColor}; + border-bottom-style: ${props => props.borderBottomStyle}; + border-width: ${props => props.borderWidth}; + background-color: ${props => props.backgroundColor}; +`; + +StandardIconRow.defaultProps = { + minHeight: '85px', + borderBottomColor: colors.walletBorder, + borderBottomStyle: 'solid', + borderWidth: '1px', + backgroundColor: colors.walletDefaultItemBackground, +}; + +StandardIconRow.displayName = 'StandardIconRow'; diff --git a/packages/website/ts/components/wallet/wallet.tsx b/packages/website/ts/components/wallet/wallet.tsx index dc48d6619..1f1e3598a 100644 --- a/packages/website/ts/components/wallet/wallet.tsx +++ b/packages/website/ts/components/wallet/wallet.tsx @@ -1,36 +1,31 @@ -import { - constants as sharedConstants, - EtherscanLinkSuffixes, - Styles, - utils as sharedUtils, -} from '@0xproject/react-shared'; +import { constants as sharedConstants, EtherscanLinkSuffixes, utils as sharedUtils } from '@0xproject/react-shared'; import { BigNumber, errorUtils } from '@0xproject/utils'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; import * as _ from 'lodash'; -import CircularProgress from 'material-ui/CircularProgress'; -import FloatingActionButton from 'material-ui/FloatingActionButton'; -import { ListItem } from 'material-ui/List'; import ActionAccountBalanceWallet from 'material-ui/svg-icons/action/account-balance-wallet'; -import ContentAdd from 'material-ui/svg-icons/content/add'; -import ContentRemove from 'material-ui/svg-icons/content/remove'; import * as React from 'react'; import { Link } from 'react-router-dom'; import firstBy = require('thenby'); import { Blockchain } from 'ts/blockchain'; +import { AccountConnection } from 'ts/components/ui/account_connection'; import { Container } from 'ts/components/ui/container'; import { IconButton } from 'ts/components/ui/icon_button'; import { Identicon } from 'ts/components/ui/identicon'; import { Island } from 'ts/components/ui/island'; +import { Text } from 'ts/components/ui/text'; import { TokenIcon } from 'ts/components/ui/token_icon'; -import { WalletDisconnectedItem } from 'ts/components/wallet/wallet_disconnected_item'; +import { BodyOverlay } from 'ts/components/wallet/body_overlay'; +import { NullTokenRow } from 'ts/components/wallet/null_token_row'; +import { PlaceHolder } from 'ts/components/wallet/placeholder'; +import { StandardIconRow } from 'ts/components/wallet/standard_icon_row'; import { WrapEtherItem } from 'ts/components/wallet/wrap_ether_item'; import { AllowanceToggle } from 'ts/containers/inputs/allowance_toggle'; import { Dispatcher } from 'ts/redux/dispatcher'; import { colors } from 'ts/style/colors'; -import { styled } from 'ts/style/theme'; import { + AccountState, BlockchainErrs, ProviderType, ScreenWidths, @@ -44,7 +39,6 @@ import { import { analytics } from 'ts/utils/analytics'; import { constants } from 'ts/utils/constants'; import { utils } from 'ts/utils/utils'; -import { styles as walletItemStyles } from 'ts/utils/wallet_item_styles'; export interface WalletProps { userAddress: string; @@ -84,66 +78,16 @@ interface AccessoryItemConfig { allowanceToggleConfig?: AllowanceToggleConfig; } -const styles: Styles = { - root: { - width: '100%', - }, - footerItemInnerDiv: { - paddingLeft: 24, - borderTopColor: colors.walletBorder, - borderTopStyle: 'solid', - borderWidth: 1, - }, - borderedItem: { - borderBottomColor: colors.walletBorder, - borderBottomStyle: 'solid', - borderWidth: 1, - }, - tokenItem: { - backgroundColor: colors.walletDefaultItemBackground, - minHeight: 85, - }, - amountLabel: { - fontWeight: 'bold', - color: colors.black, - }, - valueLabel: { - color: colors.grey, - fontSize: 14, - }, - paddedItem: { - paddingTop: 8, - paddingBottom: 8, - }, - bodyInnerDiv: { - overflow: 'auto', - WebkitOverflowScrolling: 'touch', - }, - manageYourWalletText: { - color: colors.mediumBlue, - fontWeight: 'bold', - }, - loadingBody: { - height: 381, - }, -}; - const ETHER_ICON_PATH = '/images/ether.png'; const ICON_DIMENSION = 28; const BODY_ITEM_KEY = 'BODY'; const HEADER_ITEM_KEY = 'HEADER'; -const FOOTER_ITEM_KEY = 'FOOTER'; -const DISCONNECTED_ITEM_KEY = 'DISCONNECTED'; const ETHER_ITEM_KEY = 'ETHER'; const USD_DECIMAL_PLACES = 2; const NO_ALLOWANCE_TOGGLE_SPACE_WIDTH = 56; const ACCOUNT_PATH = `${WebsitePaths.Portal}/account`; - -const ActionButton = styled(FloatingActionButton)` - button { - position: static !important; - } -`; +const PLACEHOLDER_COLOR = colors.grey300; +const LOADING_ROWS_COUNT = 6; export class Wallet extends React.Component<WalletProps, WalletState> { public static defaultProps = { @@ -156,73 +100,118 @@ export class Wallet extends React.Component<WalletProps, WalletState> { isHoveringSidebar: false, }; } + public componentDidUpdate(prevProps: WalletProps): void { + const currentTrackedTokens = this.props.trackedTokens; + const differentTrackedTokens = _.difference(currentTrackedTokens, prevProps.trackedTokens); + const firstDifferentTrackedToken = _.head(differentTrackedTokens); + // check if there is only one different token, and if that token is a member of the current tracked tokens + // this means that the token was added, not removed + if ( + !_.isUndefined(firstDifferentTrackedToken) && + _.size(differentTrackedTokens) === 1 && + _.includes(currentTrackedTokens, firstDifferentTrackedToken) + ) { + document.getElementById(firstDifferentTrackedToken.address).scrollIntoView(); + } + } public render(): React.ReactNode { - const isBlockchainLoaded = this.props.blockchainIsLoaded && this.props.blockchainErr === BlockchainErrs.NoError; return ( - <Island className="flex flex-column wallet" style={{ ...styles.root, ...this.props.style }}> - {isBlockchainLoaded ? this._renderLoadedRows() : this._renderLoadingRows()} + <Island className="flex flex-column wallet" style={this.props.style}> + {this._isBlockchainReady() ? this._renderLoadedRows() : this._renderLoadingRows()} </Island> ); } - private _renderLoadedRows(): React.ReactNode { - const isAddressAvailable = !_.isEmpty(this.props.userAddress); - return isAddressAvailable - ? _.concat(this._renderConnectedHeaderRows(), this._renderBody(), this._renderFooterRows()) - : _.concat(this._renderDisconnectedHeaderRows(), this._renderDisconnectedRows()); - } private _renderLoadingRows(): React.ReactNode { - return _.concat(this._renderDisconnectedHeaderRows(), this._renderLoadingBodyRows()); + return _.concat(this._renderLoadingHeaderRows(), this._renderLoadingBodyRows()); + } + private _renderLoadingHeaderRows(): React.ReactElement<{}> { + return this._renderPlainHeaderRow('Loading...'); } private _renderLoadingBodyRows(): React.ReactElement<{}> { + const bodyStyle = this._getBodyStyle(); + const loadingRowsRange = _.range(LOADING_ROWS_COUNT); return ( - <div key={BODY_ITEM_KEY} className="flex items-center" style={styles.loadingBody}> - <div className="mx-auto"> - <CircularProgress size={40} thickness={5} /> - </div> + <div key={BODY_ITEM_KEY} className="flex flex-column" style={bodyStyle}> + {_.map(loadingRowsRange, index => { + return <NullTokenRow key={index} iconDimension={ICON_DIMENSION} fillColor={PLACEHOLDER_COLOR} />; + })} + <Container + className="flex items-center" + position="absolute" + width="100%" + height="100%" + maxHeight={bodyStyle.maxHeight} + > + <div className="mx-auto"> + <BodyOverlay + dispatcher={this.props.dispatcher} + userAddress={this.props.userAddress} + injectedProviderName={this.props.injectedProviderName} + providerType={this.props.providerType} + onToggleLedgerDialog={this.props.onToggleLedgerDialog} + blockchain={this.props.blockchain} + blockchainIsLoaded={this.props.blockchainIsLoaded} + /> + </div> + </Container> </div> ); } + private _renderLoadedRows(): React.ReactNode { + const isAddressAvailable = !_.isEmpty(this.props.userAddress); + return isAddressAvailable + ? _.concat(this._renderConnectedHeaderRows(), this._renderBody()) + : _.concat(this._renderDisconnectedHeaderRows(), this._renderLoadingBodyRows()); + } private _renderDisconnectedHeaderRows(): React.ReactElement<{}> { - const primaryText = 'wallet'; - return ( - <StandardIconRow - key={HEADER_ITEM_KEY} - icon={<ActionAccountBalanceWallet color={colors.mediumBlue} />} - main={primaryText.toUpperCase()} - style={styles.borderedItem} - /> + const isExternallyInjectedProvider = utils.isExternallyInjected( + this.props.providerType, + this.props.injectedProviderName, ); + const text = isExternallyInjectedProvider ? 'Please unlock MetaMask...' : 'Please connect a wallet...'; + return this._renderPlainHeaderRow(text); } - private _renderDisconnectedRows(): React.ReactElement<{}> { + private _renderPlainHeaderRow(text: string): React.ReactElement<{}> { return ( - <WalletDisconnectedItem - key={DISCONNECTED_ITEM_KEY} - providerType={this.props.providerType} - injectedProviderName={this.props.injectedProviderName} - onToggleLedgerDialog={this.props.onToggleLedgerDialog} + <StandardIconRow + key={HEADER_ITEM_KEY} + icon={<ActionAccountBalanceWallet color={colors.grey} />} + main={ + <Text fontSize="16px" fontColor={colors.grey}> + {text} + </Text> + // https://github.com/palantir/tslint-react/issues/140 + // tslint:disable-next-line:jsx-curly-spacing + } + minHeight="60px" + backgroundColor={colors.white} /> ); } private _renderConnectedHeaderRows(): React.ReactElement<{}> { const userAddress = this.props.userAddress; - const primaryText = utils.getAddressBeginAndEnd(userAddress); + const accountState = this._getAccountState(); + const main = ( + <div className="flex flex-column"> + <Text fontSize="16px" lineHeight="19px" fontWeight={500}> + {utils.getAddressBeginAndEnd(userAddress)} + </Text> + <AccountConnection accountState={accountState} injectedProviderName={this.props.injectedProviderName} /> + </div> + ); return ( <Link key={HEADER_ITEM_KEY} to={ACCOUNT_PATH} style={{ textDecoration: 'none' }}> <StandardIconRow icon={<Identicon address={userAddress} diameter={ICON_DIMENSION} />} - main={primaryText} - style={styles.borderedItem} + main={main} + minHeight="60px" + backgroundColor={colors.white} /> </Link> ); } private _renderBody(): React.ReactElement<{}> { - const bodyStyle: React.CSSProperties = { - ...styles.bodyInnerDiv, - overflow: this.state.isHoveringSidebar ? 'auto' : 'hidden', - // TODO: make this completely responsive - maxHeight: this.props.screenWidth !== ScreenWidths.Sm ? 475 : undefined, - }; + const bodyStyle = this._getBodyStyle(); return ( <div style={bodyStyle} @@ -235,6 +224,17 @@ export class Wallet extends React.Component<WalletProps, WalletState> { </div> ); } + private _getBodyStyle(): React.CSSProperties { + return { + overflow: 'auto', + WebkitOverflowScrolling: 'touch', + position: 'relative', + overflowY: this.state.isHoveringSidebar ? 'scroll' : 'hidden', + marginRight: this.state.isHoveringSidebar ? 0 : 4, + // TODO: make this completely responsive + maxHeight: this.props.screenWidth !== ScreenWidths.Sm ? 475 : undefined, + }; + } private _onSidebarHover(_event: React.FormEvent<HTMLInputElement>): void { this.setState({ isHoveringSidebar: true, @@ -245,51 +245,6 @@ export class Wallet extends React.Component<WalletProps, WalletState> { isHoveringSidebar: false, }); } - private _renderFooterRows(): React.ReactElement<{}> { - return ( - <div key={FOOTER_ITEM_KEY}> - <ListItem - primaryText={ - <div className="flex"> - <ActionButton mini={true} zDepth={0} onClick={this.props.onAddToken}> - <ContentAdd /> - </ActionButton> - <ActionButton mini={true} zDepth={0} className="px1" onClick={this.props.onRemoveToken}> - <ContentRemove /> - </ActionButton> - <div - style={{ - paddingLeft: 10, - position: 'relative', - top: '50%', - transform: 'translateY(33%)', - }} - > - add/remove tokens - </div> - </div> - } - disabled={true} - innerDivStyle={styles.footerItemInnerDiv} - style={styles.borderedItem} - /> - {this.props.location.pathname !== ACCOUNT_PATH && ( - <Link to={ACCOUNT_PATH} style={{ textDecoration: 'none' }}> - <ListItem - primaryText={ - <div className="flex right" style={styles.manageYourWalletText}> - manage your wallet - </div> - // https://github.com/palantir/tslint-react/issues/140 - // tslint:disable-next-line:jsx-curly-spacing - } - style={{ ...styles.paddedItem, ...styles.borderedItem }} - /> - </Link> - )} - </div> - ); - } private _renderEthRows(): React.ReactNode { const icon = <img style={{ width: ICON_DIMENSION, height: ICON_DIMENSION }} src={ETHER_ICON_PATH} />; const primaryText = this._renderAmount( @@ -311,18 +266,18 @@ export class Wallet extends React.Component<WalletProps, WalletState> { wrappedEtherDirection: Side.Deposit, }; const key = ETHER_ITEM_KEY; - return this._renderBalanceRow(key, icon, primaryText, secondaryText, accessoryItemConfig, false, 'eth-row'); + return this._renderBalanceRow(key, icon, primaryText, secondaryText, accessoryItemConfig); } private _renderTokenRows(): React.ReactNode { const trackedTokens = this.props.trackedTokens; const trackedTokensStartingWithEtherToken = trackedTokens.sort( firstBy((t: Token) => t.symbol !== constants.ETHER_TOKEN_SYMBOL) .thenBy((t: Token) => t.symbol !== constants.ZRX_TOKEN_SYMBOL) - .thenBy('address'), + .thenBy('trackedTimestamp'), ); return _.map(trackedTokensStartingWithEtherToken, this._renderTokenRow.bind(this)); } - private _renderTokenRow(token: Token, index: number): React.ReactNode { + private _renderTokenRow(token: Token): React.ReactNode { const tokenState = this.props.trackedTokenStateByAddress[token.address]; if (_.isUndefined(tokenState)) { return null; @@ -350,16 +305,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> { }, }; const key = token.address; - const isLastRow = index === this.props.trackedTokens.length - 1; - return this._renderBalanceRow( - key, - icon, - primaryText, - secondaryText, - accessoryItemConfig, - isLastRow, - isWeth ? 'weth-row' : undefined, - ); + return this._renderBalanceRow(key, icon, primaryText, secondaryText, accessoryItemConfig); } private _renderBalanceRow( key: string, @@ -367,23 +313,15 @@ export class Wallet extends React.Component<WalletProps, WalletState> { primaryText: React.ReactNode, secondaryText: React.ReactNode, accessoryItemConfig: AccessoryItemConfig, - isLastRow: boolean, className?: string, ): React.ReactNode { const shouldShowWrapEtherItem = !_.isUndefined(this.state.wrappedEtherDirection) && this.state.wrappedEtherDirection === accessoryItemConfig.wrappedEtherDirection && !_.isUndefined(this.props.userEtherBalanceInWei); - let additionalStyle; - if (shouldShowWrapEtherItem) { - additionalStyle = walletItemStyles.focusedItem; - } else if (!isLastRow) { - additionalStyle = styles.borderedItem; - } - const style = { ...styles.tokenItem, ...additionalStyle }; const etherToken = this._getEthToken(); return ( - <div key={key} className={`flex flex-column ${className || ''}`}> + <div id={key} key={key} className={`flex flex-column ${className || ''}`}> <StandardIconRow icon={icon} main={ @@ -393,7 +331,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> { </div> } accessory={this._renderAccessoryItems(accessoryItemConfig)} - style={style} + backgroundColor={shouldShowWrapEtherItem ? colors.walletFocusedItemBackground : undefined} /> {shouldShowWrapEtherItem && ( <WrapEtherItem @@ -452,13 +390,19 @@ export class Wallet extends React.Component<WalletProps, WalletState> { ): React.ReactNode { if (isLoading) { return ( - <PlaceHolder hideChildren={isLoading}> - <div style={styles.amountLabel}>0.00 XXX</div> + <PlaceHolder hideChildren={isLoading} fillColor={PLACEHOLDER_COLOR}> + <Text fontSize="16px" fontWeight="bold" lineHeight="1em"> + 0.00 XXX + </Text> </PlaceHolder> ); } else { const result = utils.getFormattedAmount(amount, decimals, symbol); - return <div style={styles.amountLabel}>{result}</div>; + return ( + <Text fontSize="16px" fontWeight="bold" lineHeight="1em"> + {result} + </Text> + ); } } private _renderValue( @@ -481,8 +425,10 @@ export class Wallet extends React.Component<WalletProps, WalletState> { result = '$0.00'; } return ( - <PlaceHolder hideChildren={isLoading}> - <div style={styles.valueLabel}>{result}</div> + <PlaceHolder hideChildren={isLoading} fillColor={PLACEHOLDER_COLOR}> + <Text fontSize="14px" fontColor={colors.darkGrey} lineHeight="1em"> + {result} + </Text> </PlaceHolder> ); } @@ -535,41 +481,17 @@ export class Wallet extends React.Component<WalletProps, WalletState> { private _getEthToken(): Token { return utils.getEthToken(this.props.tokenByAddress); } + private _isBlockchainReady(): boolean { + return this.props.blockchainIsLoaded && !_.isUndefined(this.props.blockchain); + } + private _getAccountState(): AccountState { + return utils.getAccountState( + this._isBlockchainReady(), + this.props.providerType, + this.props.injectedProviderName, + this.props.userAddress, + ); + } } -interface StandardIconRowProps { - icon: React.ReactNode; - main: React.ReactNode; - accessory?: React.ReactNode; - style?: React.CSSProperties; -} -const StandardIconRow = (props: StandardIconRowProps) => { - return ( - <div className="flex items-center" style={props.style}> - <div className="p2">{props.icon}</div> - <div className="flex-none pr2 pt2 pb2">{props.main}</div> - <div className="flex-auto" /> - <div>{props.accessory}</div> - </div> - ); -}; -interface PlaceHolderProps { - hideChildren: React.ReactNode; - children?: React.ReactNode; -} -const PlaceHolder = (props: PlaceHolderProps) => { - const rootBackgroundColor = props.hideChildren ? colors.lightGrey : 'transparent'; - const rootStyle: React.CSSProperties = { - backgroundColor: rootBackgroundColor, - display: 'inline-block', - borderRadius: 2, - }; - const childrenVisibility = props.hideChildren ? 'hidden' : 'visible'; - const childrenStyle: React.CSSProperties = { visibility: childrenVisibility }; - return ( - <div style={rootStyle}> - <div style={childrenStyle}>{props.children}</div> - </div> - ); -}; // tslint:disable:max-file-line-count diff --git a/packages/website/ts/components/wallet/wallet_disconnected_item.tsx b/packages/website/ts/components/wallet/wallet_disconnected_item.tsx deleted file mode 100644 index 1015dce29..000000000 --- a/packages/website/ts/components/wallet/wallet_disconnected_item.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { Styles } from '@0xproject/react-shared'; -import FlatButton from 'material-ui/FlatButton'; -import * as React from 'react'; - -import { colors } from 'ts/style/colors'; -import { ProviderType } from 'ts/types'; -import { constants } from 'ts/utils/constants'; -import { utils } from 'ts/utils/utils'; - -export interface WalletDisconnectedItemProps { - providerType: ProviderType; - injectedProviderName: string; - onToggleLedgerDialog: () => void; -} - -const styles: Styles = { - button: { - border: colors.walletBorder, - borderStyle: 'solid', - borderWidth: 1, - height: 80, - }, - hrefAdjustment: { - paddingTop: 20, // HACK: For some reason when we set the href prop of a FlatButton material-ui reduces the top padding - }, - otherWalletText: { - fontSize: 14, - color: colors.grey500, - textDecoration: 'underline', - }, -}; - -const ITEM_HEIGHT = 381; -const METAMASK_ICON_WIDTH = 35; -const LEDGER_ICON_WIDTH = 30; -const BUTTON_BOTTOM_PADDING = 80; - -export const WalletDisconnectedItem: React.StatelessComponent<WalletDisconnectedItemProps> = ( - props: WalletDisconnectedItemProps, -) => { - const isExternallyInjectedProvider = utils.isExternallyInjected(props.providerType, props.injectedProviderName); - return ( - <div className="flex flex-center"> - <div className="mx-auto"> - <div className="table" style={{ height: ITEM_HEIGHT }}> - <div className="table-cell align-middle"> - <ProviderButton isExternallyInjectedProvider={isExternallyInjectedProvider} /> - <div className="flex flex-center py2" style={{ paddingBottom: BUTTON_BOTTOM_PADDING }}> - <div className="mx-auto"> - <div onClick={props.onToggleLedgerDialog} style={{ cursor: 'pointer' }}> - <img src="/images/ledger_icon.png" style={{ width: LEDGER_ICON_WIDTH }} /> - <span className="px1" style={styles.otherWalletText}> - user other wallet - </span> - </div> - </div> - </div> - </div> - </div> - </div> - </div> - ); -}; - -interface ProviderButtonProps { - isExternallyInjectedProvider: boolean; -} - -const ProviderButton: React.StatelessComponent<ProviderButtonProps> = (props: ProviderButtonProps) => ( - <FlatButton - label={props.isExternallyInjectedProvider ? 'Please unlock account' : 'Get Metamask Wallet Extension'} - labelStyle={{ color: colors.black }} - labelPosition="after" - primary={true} - icon={<img src="/images/metamask_icon.png" width={METAMASK_ICON_WIDTH.toString()} />} - style={props.isExternallyInjectedProvider ? styles.button : { ...styles.button, ...styles.hrefAdjustment }} - href={props.isExternallyInjectedProvider ? undefined : constants.URL_METAMASK_CHROME_STORE} - target={props.isExternallyInjectedProvider ? undefined : '_blank'} - disabled={props.isExternallyInjectedProvider} - /> -); diff --git a/packages/website/ts/components/wallet/wrap_ether_item.tsx b/packages/website/ts/components/wallet/wrap_ether_item.tsx index d6135ce4d..851b35f90 100644 --- a/packages/website/ts/components/wallet/wrap_ether_item.tsx +++ b/packages/website/ts/components/wallet/wrap_ether_item.tsx @@ -15,7 +15,6 @@ import { analytics } from 'ts/utils/analytics'; import { constants } from 'ts/utils/constants'; import { errorReporter } from 'ts/utils/error_reporter'; import { utils } from 'ts/utils/utils'; -import { styles as walletItemStyles } from 'ts/utils/wallet_item_styles'; export interface WrapEtherItemProps { userAddress: string; @@ -95,7 +94,7 @@ export class WrapEtherItem extends React.Component<WrapEtherItemProps, WrapEther const topLabelText = isWrappingEth ? 'Convert ETH into WETH 1:1' : 'Convert WETH into ETH 1:1'; return ( - <div className="flex" style={walletItemStyles.focusedItem}> + <div className="flex" style={{ backgroundColor: colors.walletFocusedItemBackground }}> <div>{this._renderIsEthConversionHappeningSpinner()} </div> <div className="flex flex-column"> <div style={styles.topLabel}>{topLabelText}</div> @@ -173,6 +172,7 @@ export class WrapEtherItem extends React.Component<WrapEtherItemProps, WrapEther <FlatButton backgroundColor={colors.wrapEtherConfirmationButton} label={labelText} + style={{ zIndex: 0 }} labelStyle={styles.wrapEtherConfirmationButtonLabel} onClick={this._wrapEtherConfirmationActionAsync.bind(this)} disabled={this.state.isEthConversionHappening} diff --git a/packages/website/ts/containers/portal_onboarding_flow.ts b/packages/website/ts/containers/portal_onboarding_flow.ts index 12daad021..a813205b1 100644 --- a/packages/website/ts/containers/portal_onboarding_flow.ts +++ b/packages/website/ts/containers/portal_onboarding_flow.ts @@ -19,7 +19,7 @@ interface ConnectedState { stepIndex: number; isRunning: boolean; userAddress: string; - hasBeenSeen: boolean; + hasBeenClosed: boolean; providerType: ProviderType; injectedProviderName: string; blockchainIsLoaded: boolean; @@ -43,7 +43,7 @@ const mapStateToProps = (state: State, _ownProps: PortalOnboardingFlowProps): Co blockchainIsLoaded: state.blockchainIsLoaded, userEtherBalanceInWei: state.userEtherBalanceInWei, tokenByAddress: state.tokenByAddress, - hasBeenSeen: state.hasPortalOnboardingBeenSeen, + hasBeenClosed: state.hasPortalOnboardingBeenClosed, screenWidth: state.screenWidth, }); diff --git a/packages/website/ts/index.tsx b/packages/website/ts/index.tsx index 249b4fdc9..c7ccfdf1f 100644 --- a/packages/website/ts/index.tsx +++ b/packages/website/ts/index.tsx @@ -143,6 +143,7 @@ render( component={LazySolCompilerDocumentation} /> + <Route path={`${WebsitePaths.Docs}`} component={LazyZeroExJSDocumentation} /> <Route component={NotFound as any} /> </Switch> </div> diff --git a/packages/website/ts/pages/jobs/list/list_item.tsx b/packages/website/ts/pages/jobs/list/list_item.tsx index d7838bc01..192433d39 100644 --- a/packages/website/ts/pages/jobs/list/list_item.tsx +++ b/packages/website/ts/pages/jobs/list/list_item.tsx @@ -1,14 +1,14 @@ import * as React from 'react'; +import { Circle } from 'ts/components/ui/circle'; + export interface ListItemProps { bulletColor?: string; } export const ListItem: React.StatelessComponent<ListItemProps> = ({ bulletColor, children }) => { return ( <div className="flex items-center"> - <svg className="flex-none lg-px2 md-px2 sm-pl2" height="26" width="26"> - <circle cx="13" cy="13" r="13" fill={bulletColor || 'transparent'} /> - </svg> + <Circle className="flex-none lg-px2 md-px2 sm-pl2" diameter={26} fillColor={bulletColor || 'transparent'} /> <div className="flex-auto px2">{children}</div> </div> ); diff --git a/packages/website/ts/pages/not_found.tsx b/packages/website/ts/pages/not_found.tsx index 2fe3b1f45..a7992a8fa 100644 --- a/packages/website/ts/pages/not_found.tsx +++ b/packages/website/ts/pages/not_found.tsx @@ -11,15 +11,15 @@ export interface NotFoundProps { dispatcher: Dispatcher; } -export const NotFound = (_props: NotFoundProps) => { +export const NotFound = (props: NotFoundProps) => { return ( <div> - <TopBar blockchainIsLoaded={false} location={this.props.location} translate={this.props.translate} /> + <TopBar blockchainIsLoaded={false} location={props.location} translate={props.translate} /> <FullscreenMessage headerText={'404 Not Found'} bodyText={"Hm... looks like we couldn't find what you are looking for."} /> - <Footer translate={this.props.translate} dispatcher={this.props.dispatcher} /> + <Footer translate={props.translate} dispatcher={props.dispatcher} /> </div> ); }; diff --git a/packages/website/ts/redux/reducer.ts b/packages/website/ts/redux/reducer.ts index ed6a4868e..caddabcf0 100644 --- a/packages/website/ts/redux/reducer.ts +++ b/packages/website/ts/redux/reducer.ts @@ -42,7 +42,7 @@ export interface State { userEtherBalanceInWei?: BigNumber; portalOnboardingStep: number; isPortalOnboardingShowing: boolean; - hasPortalOnboardingBeenSeen: boolean; + hasPortalOnboardingBeenClosed: boolean; // Note: cache of supplied orderJSON in fill order step. Do not use for anything else. userSuppliedOrderCache: Order; @@ -85,7 +85,7 @@ export const INITIAL_STATE: State = { userSuppliedOrderCache: undefined, portalOnboardingStep: 0, isPortalOnboardingShowing: false, - hasPortalOnboardingBeenSeen: false, + hasPortalOnboardingBeenClosed: false, // Docs docsVersion: DEFAULT_DOCS_VERSION, availableDocVersions: [DEFAULT_DOCS_VERSION], @@ -311,7 +311,9 @@ export function reducer(state: State = INITIAL_STATE, action: Action): State { return { ...state, isPortalOnboardingShowing, - hasPortalOnboardingBeenSeen: true, + hasPortalOnboardingBeenClosed: !isPortalOnboardingShowing ? true : state.hasPortalOnboardingBeenClosed, + // always start onboarding from the beginning + portalOnboardingStep: 0, }; } diff --git a/packages/website/ts/redux/store.ts b/packages/website/ts/redux/store.ts index 203f068a1..0d0e6cea1 100644 --- a/packages/website/ts/redux/store.ts +++ b/packages/website/ts/redux/store.ts @@ -15,7 +15,7 @@ store.subscribe( _.throttle(() => { // Persisted state stateStorage.saveState({ - hasPortalOnboardingBeenSeen: store.getState().hasPortalOnboardingBeenSeen, + hasPortalOnboardingBeenClosed: store.getState().hasPortalOnboardingBeenClosed, }); }, ONE_SECOND), ); diff --git a/packages/website/ts/style/colors.ts b/packages/website/ts/style/colors.ts index 45be4fe7f..349845a09 100644 --- a/packages/website/ts/style/colors.ts +++ b/packages/website/ts/style/colors.ts @@ -6,13 +6,13 @@ const appColors = { walletDefaultItemBackground: '#fbfbfc', walletFocusedItemBackground: '#f0f1f4', allowanceToggleShadow: 'rgba(0, 0, 0, 0)', - allowanceToggleOffTrack: '#adadad', - allowanceToggleOnTrack: sharedColors.mediumBlue, wrapEtherConfirmationButton: sharedColors.mediumBlue, drawerMenuBackground: '#4a4a4a', menuItemDefaultSelectedBackground: '#424242', jobsPageBackground: sharedColors.grey50, jobsPageOpenPositionRow: sharedColors.grey100, + metaMaskOrange: '#f68c24', + metaMaskTransparentOrange: 'rgba(255, 248, 242, 0.8)', }; export const colors = { diff --git a/packages/website/ts/types.ts b/packages/website/ts/types.ts index d00154652..498a0a5b8 100644 --- a/packages/website/ts/types.ts +++ b/packages/website/ts/types.ts @@ -13,8 +13,8 @@ export interface Token { address: string; symbol: string; decimals: number; - isTracked: boolean; isRegistered: boolean; + trackedTimestamp?: number; } export interface TokenByAddress { @@ -356,6 +356,7 @@ export enum WebsiteLegacyPaths { export enum WebsitePaths { Portal = '/portal', Wiki = '/wiki', + Docs = '/docs', ZeroExJs = '/docs/0x.js', Home = '/', FAQ = '/faq', @@ -488,6 +489,16 @@ export enum Providers { Mist = 'MIST', } +export interface InjectedProviderUpdate { + selectedAddress: string; + networkVersion: string; +} + +export interface InjectedProviderObservable { + subscribe(updateHandler: (update: InjectedProviderUpdate) => void): void; + unsubscribe(updateHandler: (update: InjectedProviderUpdate) => void): void; +} + export interface TimestampMsRange { startTimestampMs: number; endTimestampMs: number; @@ -546,4 +557,18 @@ export interface WebsiteBackendJobInfo { office: string; url: string; } + +export enum BrowserType { + Chrome = 'Chrome', + Firefox = 'Firefox', + Opera = 'Opera', + Other = 'Other', +} + +export enum AccountState { + Disconnected = 'Disconnected', + Ready = 'Ready', + Loading = 'Loading', + Locked = 'Locked', +} // tslint:disable:max-file-line-count diff --git a/packages/website/ts/utils/configs.ts b/packages/website/ts/utils/configs.ts index ace8a5ba0..e8a486c35 100644 --- a/packages/website/ts/utils/configs.ts +++ b/packages/website/ts/utils/configs.ts @@ -11,7 +11,7 @@ const INFURA_API_KEY = 'T5WSC8cautR4KXyYgsRs'; export const configs = { AMOUNT_DISPLAY_PRECSION: 5, BACKEND_BASE_PROD_URL: 'https://website-api.0xproject.com', - BACKEND_BASE_STAGING_URL: 'http://ec2-52-91-181-85.compute-1.amazonaws.com', + BACKEND_BASE_STAGING_URL: 'https://staging-website-api.0xproject.com', BASE_URL, BITLY_ACCESS_TOKEN: 'ffc4c1a31e5143848fb7c523b39f91b9b213d208', DEFAULT_DERIVATION_PATH: `44'/60'/0'`, @@ -63,10 +63,9 @@ export const configs = { TKN: '/images/token_icons/tokencard.png', TRST: '/images/token_icons/trust.png', } as { [symbol: string]: string }, - IS_MAINNET_ENABLED: true, GOOGLE_ANALYTICS_ID: 'UA-98720122-1', LAST_LOCAL_STORAGE_FILL_CLEARANCE_DATE: '2017-11-22', - LAST_LOCAL_STORAGE_TRACKED_TOKEN_CLEARANCE_DATE: '2017-12-19', + LAST_LOCAL_STORAGE_TRACKED_TOKEN_CLEARANCE_DATE: '2018-6-25', OUTDATED_WRAPPED_ETHERS: [ { 42: { diff --git a/packages/website/ts/utils/constants.ts b/packages/website/ts/utils/constants.ts index 25670ef27..d3a2aa107 100644 --- a/packages/website/ts/utils/constants.ts +++ b/packages/website/ts/utils/constants.ts @@ -26,7 +26,7 @@ export const constants = { NETWORK_ID_TESTRPC: 50, NULL_ADDRESS: '0x0000000000000000000000000000000000000000', PROVIDER_NAME_LEDGER: 'Ledger', - PROVIDER_NAME_METAMASK: 'Metamask', + PROVIDER_NAME_METAMASK: 'MetaMask', PROVIDER_NAME_PARITY_SIGNER: 'Parity Signer', PROVIDER_NAME_MIST: 'Mist', PROVIDER_NAME_GENERIC: 'Injected Web3', @@ -70,6 +70,9 @@ export const constants = { URL_GITHUB_ORG: 'https://github.com/0xProject', URL_GITHUB_WIKI: 'https://github.com/0xProject/wiki', URL_METAMASK_CHROME_STORE: 'https://chrome.google.com/webstore/detail/metamask/nkbihfbeogaeaoehlefnkodbefgpgknn', + URL_METAMASK_FIREFOX_STORE: 'https://addons.mozilla.org/en-US/firefox/addon/ether-metamask/', + URL_METAMASK_HOMEPAGE: 'https://metamask.io/', + URL_METAMASK_OPERA_STORE: 'https://addons.opera.com/en/extensions/details/metamask/', URL_MIST_DOWNLOAD: 'https://github.com/ethereum/mist/releases', URL_PARITY_CHROME_STORE: 'https://chrome.google.com/webstore/detail/parity-ethereum-integrati/himekenlppkgeaoeddcliojfddemadig', diff --git a/packages/website/ts/utils/utils.ts b/packages/website/ts/utils/utils.ts index 0cb965f05..726e1815f 100644 --- a/packages/website/ts/utils/utils.ts +++ b/packages/website/ts/utils/utils.ts @@ -4,11 +4,14 @@ import { constants as sharedConstants, Networks } from '@0xproject/react-shared' import { ECSignature, Provider } from '@0xproject/types'; import { BigNumber } from '@0xproject/utils'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import * as bowser from 'bowser'; import deepEqual = require('deep-equal'); import * as _ from 'lodash'; import * as moment from 'moment'; import { + AccountState, BlockchainCallErrs, + BrowserType, Environments, Order, Providers, @@ -190,23 +193,37 @@ export const utils = { const truncatedAddress = `${address.substring(0, 6)}...${address.substr(-4)}`; // 0x3d5a...b287 return truncatedAddress; }, - getReadableAccountState( + getReadableAccountState(accountState: AccountState, userAddress: string): string { + switch (accountState) { + case AccountState.Loading: + return 'Loading...'; + case AccountState.Ready: + return utils.getAddressBeginAndEnd(userAddress); + case AccountState.Locked: + return 'Please Unlock'; + case AccountState.Disconnected: + return 'Connect a Wallet'; + default: + return ''; + } + }, + getAccountState( isBlockchainReady: boolean, providerType: ProviderType, injectedProviderName: string, userAddress?: string, - ): string { + ): AccountState { const isAddressAvailable = !_.isUndefined(userAddress) && !_.isEmpty(userAddress); const isExternallyInjectedProvider = utils.isExternallyInjected(providerType, injectedProviderName); if (!isBlockchainReady) { - return 'Loading account'; + return AccountState.Loading; } else if (isAddressAvailable) { - return utils.getAddressBeginAndEnd(userAddress); + return AccountState.Ready; // tslint:disable-next-line: prefer-conditional-expression } else if (isExternallyInjectedProvider) { - return 'Account locked'; + return AccountState.Locked; } else { - return 'No wallet detected'; + return AccountState.Disconnected; } }, hasUniqueNameAndSymbol(tokens: Token[], token: Token): boolean { @@ -353,6 +370,11 @@ export const utils = { const token = _.find(tokens, { symbol }); return token; }, + getTrackedTokens(tokenByAddress: TokenByAddress): Token[] { + const allTokens = _.values(tokenByAddress); + const trackedTokens = _.filter(allTokens, t => this.isTokenTracked(t)); + return trackedTokens; + }, getFormattedAmountFromToken(token: Token, tokenState: TokenState): string { return utils.getFormattedAmount(tokenState.balance, token.decimals, token.symbol); }, @@ -368,4 +390,18 @@ export const utils = { isMobile(screenWidth: ScreenWidths): boolean { return screenWidth === ScreenWidths.Sm; }, + getBrowserType(): BrowserType { + if (bowser.chrome) { + return BrowserType.Chrome; + } else if (bowser.firefox) { + return BrowserType.Firefox; + } else if (bowser.opera) { + return BrowserType.Opera; + } else { + return BrowserType.Other; + } + }, + isTokenTracked(token: Token): boolean { + return !_.isUndefined(token.trackedTimestamp); + }, }; diff --git a/packages/website/ts/utils/wallet_item_styles.ts b/packages/website/ts/utils/wallet_item_styles.ts deleted file mode 100644 index 9d6033d74..000000000 --- a/packages/website/ts/utils/wallet_item_styles.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Styles } from '@0xproject/react-shared'; - -import { colors } from 'ts/style/colors'; - -export const styles: Styles = { - focusedItem: { - backgroundColor: colors.walletFocusedItemBackground, - }, -}; |