diff options
Diffstat (limited to 'packages/website/ts')
16 files changed, 233 insertions, 120 deletions
diff --git a/packages/website/ts/blockchain.ts b/packages/website/ts/blockchain.ts index 3ebdd1dee..46a4d6629 100644 --- a/packages/website/ts/blockchain.ts +++ b/packages/website/ts/blockchain.ts @@ -229,7 +229,7 @@ export class Blockchain { shouldPollUserAddress, ); this._contractWrappers.setProvider(provider, this.networkId); - this._blockchainWatcher.startEmittingNetworkConnectionAndUserBalanceState(); + await this._blockchainWatcher.startEmittingNetworkConnectionAndUserBalanceStateAsync(); this._dispatcher.updateProviderType(ProviderType.Ledger); } public async updateProviderToInjectedAsync(): Promise<void> { @@ -259,7 +259,7 @@ export class Blockchain { this._contractWrappers.setProvider(provider, this.networkId); await this.fetchTokenInformationAsync(); - this._blockchainWatcher.startEmittingNetworkConnectionAndUserBalanceState(); + await this._blockchainWatcher.startEmittingNetworkConnectionAndUserBalanceStateAsync(); this._dispatcher.updateProviderType(ProviderType.Injected); delete this._ledgerSubprovider; delete this._cachedProvider; @@ -816,7 +816,7 @@ export class Blockchain { this._userAddressIfExists = userAddresses[0]; this._dispatcher.updateUserAddress(this._userAddressIfExists); await this.fetchTokenInformationAsync(); - this._blockchainWatcher.startEmittingNetworkConnectionAndUserBalanceState(); + await this._blockchainWatcher.startEmittingNetworkConnectionAndUserBalanceStateAsync(); 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 3890a9e57..c576db6ac 100644 --- a/packages/website/ts/blockchain_watcher.ts +++ b/packages/website/ts/blockchain_watcher.ts @@ -34,56 +34,15 @@ export class BlockchainWatcher { public updatePrevUserAddress(userAddress: string): void { this._prevUserAddressIfExists = userAddress; } - public startEmittingNetworkConnectionAndUserBalanceState(): void { + public async startEmittingNetworkConnectionAndUserBalanceStateAsync(): Promise<void> { if (!_.isUndefined(this._watchNetworkAndBalanceIntervalId)) { return; // we are already emitting the state } - - let prevNodeVersion: string; this._prevUserEtherBalanceInWei = undefined; this._dispatcher.updateNetworkId(this._prevNetworkId); + await this._updateNetworkAndBalanceAsync(); this._watchNetworkAndBalanceIntervalId = intervalUtils.setAsyncExcludingInterval( - async () => { - // Check for network state changes - 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) { - prevNodeVersion = currentNodeVersion; - this._dispatcher.updateNodeVersion(currentNodeVersion); - } - - if (this._shouldPollUserAddress) { - const addresses = await this._web3Wrapper.getAvailableAddressesAsync(); - const userAddressIfExists = addresses[0]; - // Update makerAddress on network change - if (this._prevUserAddressIfExists !== userAddressIfExists) { - this._prevUserAddressIfExists = userAddressIfExists; - this._dispatcher.updateUserAddress(userAddressIfExists); - } - - // Check for user ether balance changes - if (!_.isUndefined(userAddressIfExists)) { - await this._updateUserWeiBalanceAsync(userAddressIfExists); - } - } else { - // This logic is primarily for the Ledger, since we don't regularly poll for the address - // we simply update the balance for the last fetched address. - if (!_.isUndefined(this._prevUserAddressIfExists)) { - await this._updateUserWeiBalanceAsync(this._prevUserAddressIfExists); - } - } - }, + this._updateNetworkAndBalanceAsync.bind(this), 5000, (err: Error) => { logUtils.log(`Watching network and balances failed: ${err.stack}`); @@ -91,6 +50,48 @@ export class BlockchainWatcher { }, ); } + private async _updateNetworkAndBalanceAsync(): Promise<void> { + // Check for network state changes + 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) { + prevNodeVersion = currentNodeVersion; + this._dispatcher.updateNodeVersion(currentNodeVersion); + } + + if (this._shouldPollUserAddress) { + const addresses = await this._web3Wrapper.getAvailableAddressesAsync(); + const userAddressIfExists = addresses[0]; + // Update makerAddress on network change + if (this._prevUserAddressIfExists !== userAddressIfExists) { + this._prevUserAddressIfExists = userAddressIfExists; + this._dispatcher.updateUserAddress(userAddressIfExists); + } + + // Check for user ether balance changes + if (!_.isUndefined(userAddressIfExists)) { + await this._updateUserWeiBalanceAsync(userAddressIfExists); + } + } else { + // This logic is primarily for the Ledger, since we don't regularly poll for the address + // we simply update the balance for the last fetched address. + if (!_.isUndefined(this._prevUserAddressIfExists)) { + await this._updateUserWeiBalanceAsync(this._prevUserAddressIfExists); + } + } + } private async _updateUserWeiBalanceAsync(userAddress: string): Promise<void> { const balanceInWei = await this._web3Wrapper.getBalanceInWeiAsync(userAddress); if (_.isUndefined(this._prevUserEtherBalanceInWei) || !balanceInWei.eq(this._prevUserEtherBalanceInWei)) { diff --git a/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx b/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx index 4283022e2..7e40192f6 100644 --- a/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx +++ b/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx @@ -1,5 +1,7 @@ +import { constants as sharedConstants } from '@0xproject/react-shared'; import * as _ from 'lodash'; import * as React from 'react'; +import { RouteComponentProps, withRouter } from 'react-router'; import { BigNumber } from '@0xproject/utils'; import { Blockchain } from 'ts/blockchain'; @@ -13,9 +15,11 @@ import { UnlockWalletOnboardingStep } from 'ts/components/onboarding/unlock_wall import { WrapEthOnboardingStep } from 'ts/components/onboarding/wrap_eth_onboarding_step'; import { AllowanceToggle } from 'ts/containers/inputs/allowance_toggle'; import { ProviderType, Token, TokenByAddress, TokenStateByAddress } from 'ts/types'; +import { analytics } from 'ts/utils/analytics'; import { utils } from 'ts/utils/utils'; -export interface PortalOnboardingFlowProps { +export interface PortalOnboardingFlowProps extends RouteComponentProps<any> { + networkId: number; blockchain: Blockchain; stepIndex: number; isRunning: boolean; @@ -32,9 +36,15 @@ export interface PortalOnboardingFlowProps { refetchTokenStateAsync: (tokenAddress: string) => Promise<void>; } -export class PortalOnboardingFlow extends React.Component<PortalOnboardingFlowProps> { +class PlainPortalOnboardingFlow extends React.Component<PortalOnboardingFlowProps> { + private _unlisten: () => void; public componentDidMount(): void { this._overrideOnboardingStateIfShould(); + // 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(); @@ -45,8 +55,8 @@ export class PortalOnboardingFlow extends React.Component<PortalOnboardingFlowPr steps={this._getSteps()} stepIndex={this.props.stepIndex} isRunning={this.props.isRunning} - onClose={this.props.updateIsRunning.bind(this, false)} - updateOnboardingStep={this.props.updateOnboardingStep} + onClose={this._closeOnboarding.bind(this)} + updateOnboardingStep={this._updateOnboardingStep.bind(this)} /> ); } @@ -181,9 +191,21 @@ export class PortalOnboardingFlow extends React.Component<PortalOnboardingFlowPr } private _autoStartOnboardingIfShould(): void { if (!this.props.isRunning && !this.props.hasBeenSeen && 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); } } + private _updateOnboardingStep(stepIndex: number): void { + const networkName = sharedConstants.NETWORK_NAME_BY_ID[this.props.networkId]; + this.props.updateOnboardingStep(stepIndex); + analytics.logEvent('Portal', 'Update Onboarding Step', networkName, stepIndex); + } + private _closeOnboarding(): void { + const networkName = sharedConstants.NETWORK_NAME_BY_ID[this.props.networkId]; + this.props.updateIsRunning(false); + analytics.logEvent('Portal', 'Onboarding Closed', networkName, this.props.stepIndex); + } private _renderZrxAllowanceToggle(): React.ReactNode { const zrxToken = utils.getZrxToken(this.props.tokenByAddress); return this._renderAllowanceToggle(zrxToken); @@ -209,3 +231,5 @@ export class PortalOnboardingFlow extends React.Component<PortalOnboardingFlowPr ); } } + +export const PortalOnboardingFlow = withRouter(PlainPortalOnboardingFlow); diff --git a/packages/website/ts/components/portal/drawer_menu.tsx b/packages/website/ts/components/portal/drawer_menu.tsx index 8ac2b9091..4bd07769f 100644 --- a/packages/website/ts/components/portal/drawer_menu.tsx +++ b/packages/website/ts/components/portal/drawer_menu.tsx @@ -2,10 +2,12 @@ import { Styles } from '@0xproject/react-shared'; import * as _ from 'lodash'; import * as React from 'react'; +import { Blockchain } from 'ts/blockchain'; import { defaultMenuItemEntries, Menu } from 'ts/components/portal/menu'; import { Identicon } from 'ts/components/ui/identicon'; +import { Text } from 'ts/components/ui/text'; import { colors } from 'ts/style/colors'; -import { WebsitePaths } from 'ts/types'; +import { ProviderType, WebsitePaths } from 'ts/types'; import { utils } from 'ts/utils/utils'; const IDENTICON_DIAMETER = 45; @@ -25,14 +27,15 @@ const styles: Styles = { MozBorderRadius: BORDER_RADIUS, WebkitBorderRadius: BORDER_RADIUS, }, - userAddress: { - color: colors.white, - }, }; export interface DrawerMenuProps { selectedPath?: string; userAddress?: string; + injectedProviderName: string; + providerType: ProviderType; + blockchain?: Blockchain; + blockchainIsLoaded: boolean; } export const DrawerMenu = (props: DrawerMenuProps) => { const relayerItemEntry = { @@ -41,9 +44,15 @@ export const DrawerMenu = (props: DrawerMenuProps) => { iconName: 'zmdi-portable-wifi', }; const menuItemEntries = _.concat(relayerItemEntry, defaultMenuItemEntries); + const displayMessage = utils.getReadableAccountState( + props.blockchainIsLoaded && !_.isUndefined(props.blockchain), + props.providerType, + props.injectedProviderName, + props.userAddress, + ); return ( <div style={styles.root}> - <Header userAddress={props.userAddress} /> + <Header userAddress={props.userAddress} displayMessage={displayMessage} /> <Menu selectedPath={props.selectedPath} menuItemEntries={menuItemEntries} /> </div> ); @@ -51,17 +60,16 @@ export const DrawerMenu = (props: DrawerMenuProps) => { interface HeaderProps { userAddress?: string; + displayMessage: string; } const Header = (props: HeaderProps) => { return ( <div className="flex flex-center py4"> <div className="flex flex-column mx-auto"> <Identicon address={props.userAddress} diameter={IDENTICON_DIAMETER} style={styles.identicon} /> - {!_.isUndefined(props.userAddress) && ( - <div className="pt2" style={styles.userAddress}> - {utils.getAddressBeginAndEnd(props.userAddress)} - </div> - )} + <Text className="pt2" fontColor={colors.white}> + {props.displayMessage} + </Text> </div> </div> ); diff --git a/packages/website/ts/components/portal/portal.tsx b/packages/website/ts/components/portal/portal.tsx index 28a303793..67314678b 100644 --- a/packages/website/ts/components/portal/portal.tsx +++ b/packages/website/ts/components/portal/portal.tsx @@ -1,4 +1,4 @@ -import { colors, Styles } from '@0xproject/react-shared'; +import { colors, constants as sharedConstants, Styles } from '@0xproject/react-shared'; import { BigNumber } from '@0xproject/utils'; import * as _ from 'lodash'; import ActionAccountBalanceWallet from 'material-ui/svg-icons/action/account-balance-wallet'; @@ -33,6 +33,7 @@ import { localStorage } from 'ts/local_storage/local_storage'; import { trackedTokenStorage } from 'ts/local_storage/tracked_token_storage'; import { FullscreenMessage } from 'ts/pages/fullscreen_message'; import { Dispatcher } from 'ts/redux/dispatcher'; +import { zIndex } from 'ts/style/z_index'; import { BlockchainErrs, HashData, @@ -46,6 +47,7 @@ import { TokenVisibility, WebsitePaths, } from 'ts/types'; +import { analytics } from 'ts/utils/analytics'; import { backendClient } from 'ts/utils/backend_client'; import { configs } from 'ts/utils/configs'; import { constants } from 'ts/utils/constants'; @@ -73,6 +75,8 @@ export interface PortalProps { flashMessage?: string | React.ReactNode; lastForceTokenStateRefetch: number; translate: Translate; + isPortalOnboardingShowing: boolean; + portalOnboardingStep: number; } interface PortalState { @@ -155,9 +159,6 @@ export class Portal extends React.Component<PortalProps, PortalState> { } public componentWillMount(): void { this._blockchain = new Blockchain(this.props.dispatcher); - const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress); - // tslint:disable-next-line:no-floating-promises - this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses); } public componentWillUnmount(): void { this._blockchain.destroy(); @@ -168,6 +169,13 @@ export class Portal extends React.Component<PortalProps, PortalState> { // become disconnected from their backing Ethereum node, changed user accounts, etc...) this.props.dispatcher.resetState(); } + public componentDidUpdate(prevProps: PortalProps): void { + if (!prevProps.blockchainIsLoaded && this.props.blockchainIsLoaded) { + const trackedTokenAddresses = _.keys(this.state.trackedTokenStateByAddress); + // tslint:disable-next-line:no-floating-promises + this._fetchBalancesAndAllowancesAsync(trackedTokenAddresses); + } + } public componentWillReceiveProps(nextProps: PortalProps): void { if (nextProps.networkId !== this.state.prevNetworkId) { // tslint:disable-next-line:no-floating-promises @@ -335,6 +343,7 @@ export class Portal extends React.Component<PortalProps, PortalState> { return ( <div> <Wallet + style={this.props.isPortalOnboardingShowing ? { zIndex: zIndex.aboveOverlay } : undefined} userAddress={this.props.userAddress} networkId={this.props.networkId} blockchain={this._blockchain} @@ -384,6 +393,8 @@ export class Portal extends React.Component<PortalProps, PortalState> { } private _startOnboarding(): void { + 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); } private _renderWalletSection(): React.ReactNode { 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 98d6dc0b3..23860856b 100644 --- a/packages/website/ts/components/relayer_index/relayer_grid_tile.tsx +++ b/packages/website/ts/components/relayer_index/relayer_grid_tile.tsx @@ -1,7 +1,8 @@ -import { Styles } from '@0xproject/react-shared'; +import { constants as sharedConstants, Styles } from '@0xproject/react-shared'; import * as _ from 'lodash'; import { GridTile } from 'material-ui/GridList'; import * as React from 'react'; +import { analytics } from 'ts/utils/analytics'; import { TopTokens } from 'ts/components/relayer_index/relayer_top_tokens'; import { Container } from 'ts/components/ui/container'; @@ -66,6 +67,9 @@ export const RelayerGridTile: React.StatelessComponent<RelayerGridTileProps> = ( const link = props.relayerInfo.appUrl || props.relayerInfo.url; const topTokens = props.relayerInfo.topTokens; const weeklyTxnVolume = props.relayerInfo.weeklyTxnVolume; + const networkName = sharedConstants.NETWORK_NAME_BY_ID[props.networkId]; + const eventLabel = `${props.relayerInfo.name}-${networkName}`; + const trackRelayerClick = () => analytics.logEvent('Portal', 'Relayer Click', eventLabel); const headerImageUrl = props.relayerInfo.logoImgUrl; const headerBackgroundColor = !_.isUndefined(headerImageUrl) && !_.isUndefined(props.relayerInfo.primaryColor) @@ -74,7 +78,7 @@ export const RelayerGridTile: React.StatelessComponent<RelayerGridTileProps> = ( return ( <Island style={styles.root} Component={GridTile}> <div style={styles.innerDiv}> - <a href={link} target="_blank" style={{ textDecoration: 'none' }}> + <a href={link} target="_blank" style={{ textDecoration: 'none' }} onClick={trackRelayerClick}> <div className="flex items-center" style={{ ...styles.header, backgroundColor: headerBackgroundColor }} 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 a5754180b..b599e7123 100644 --- a/packages/website/ts/components/relayer_index/relayer_top_tokens.tsx +++ b/packages/website/ts/components/relayer_index/relayer_top_tokens.tsx @@ -1,6 +1,13 @@ -import { colors, EtherscanLinkSuffixes, Styles, utils as sharedUtils } from '@0xproject/react-shared'; +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 { WebsiteBackendTokenInfo } from 'ts/types'; @@ -61,6 +68,9 @@ class TokenLink extends React.Component<TokenLinkProps, TokenLinkState> { 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 trackTokenClick = () => analytics.logEvent('Portal', 'Token Click', eventLabel); return ( <a href={tokenLinkFromToken(this.props.tokenInfo, this.props.networkId)} @@ -68,6 +78,7 @@ class TokenLink extends React.Component<TokenLinkProps, TokenLinkState> { style={style} onMouseEnter={this._onToggleHover.bind(this, true)} onMouseLeave={this._onToggleHover.bind(this, false)} + onClick={trackTokenClick} > {this.props.tokenInfo.symbol} </a> diff --git a/packages/website/ts/components/top_bar/provider_display.tsx b/packages/website/ts/components/top_bar/provider_display.tsx index cb7c9b483..1e8855c14 100644 --- a/packages/website/ts/components/top_bar/provider_display.tsx +++ b/packages/website/ts/components/top_bar/provider_display.tsx @@ -6,8 +6,11 @@ import * as React from 'react'; import { Blockchain } from 'ts/blockchain'; import { ProviderPicker } from 'ts/components/top_bar/provider_picker'; +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 { Text } from 'ts/components/ui/text'; import { Dispatcher } from 'ts/redux/dispatcher'; import { colors } from 'ts/style/colors'; import { ProviderType } from 'ts/types'; @@ -40,23 +43,16 @@ const styles: Styles = { export class ProviderDisplay extends React.Component<ProviderDisplayProps, ProviderDisplayState> { public render(): React.ReactNode { - const isAddressAvailable = !_.isEmpty(this.props.userAddress); const isExternallyInjectedProvider = utils.isExternallyInjected( this.props.providerType, this.props.injectedProviderName, ); - let displayMessage; - if (!this._isBlockchainReady()) { - displayMessage = 'loading account'; - } else if (isAddressAvailable) { - displayMessage = utils.getAddressBeginAndEnd(this.props.userAddress); - // tslint:disable-next-line: prefer-conditional-expression - } else if (isExternallyInjectedProvider) { - displayMessage = 'Account locked'; - } else { - displayMessage = '0x0000...0000'; - } - + 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 @@ -66,7 +62,7 @@ export class ProviderDisplay extends React.Component<ProviderDisplayProps, Provi this.props.providerType === ProviderType.Injected ? injectedProviderName : 'Ledger Nano S'; const isProviderMetamask = providerTitle === constants.PROVIDER_NAME_METAMASK; const hoverActiveNode = ( - <div className="flex right lg-pr0 md-pr2 sm-pr2 p1" style={styles.root}> + <div className="flex items-center p1" style={styles.root}> <div> {this._isBlockchainReady() ? ( <Identicon address={this.props.userAddress} diameter={ROOT_HEIGHT} /> @@ -74,13 +70,13 @@ export class ProviderDisplay extends React.Component<ProviderDisplayProps, Provi <CircularProgress size={ROOT_HEIGHT} thickness={2} /> )} </div> - <div style={{ marginLeft: 12, paddingTop: 3 }}> - <div style={{ fontSize: 16, color: colors.darkGrey }}>{displayMessage}</div> - </div> + <Container marginLeft="12px" marginRight="12px"> + <Text fontSize="14px" fontColor={colors.darkGrey}> + {displayMessage} + </Text> + </Container> {isProviderMetamask && ( - <div style={{ marginLeft: 16 }}> - <img src="/images/metamask_icon.png" style={{ width: ROOT_HEIGHT, height: ROOT_HEIGHT }} /> - </div> + <Image src="/images/metamask_icon.png" height={ROOT_HEIGHT} width={ROOT_HEIGHT} /> )} </div> ); diff --git a/packages/website/ts/components/top_bar/top_bar.tsx b/packages/website/ts/components/top_bar/top_bar.tsx index 1a69827a4..537edc7bb 100644 --- a/packages/website/ts/components/top_bar/top_bar.tsx +++ b/packages/website/ts/components/top_bar/top_bar.tsx @@ -297,7 +297,14 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> { openSecondary={true} onRequestChange={this._onMenuButtonClick.bind(this)} > - <DrawerMenu selectedPath={this.props.location.pathname} userAddress={this.props.userAddress} /> + <DrawerMenu + selectedPath={this.props.location.pathname} + userAddress={this.props.userAddress} + injectedProviderName={this.props.injectedProviderName} + providerType={this.props.providerType} + blockchainIsLoaded={this.props.blockchainIsLoaded} + blockchain={this.props.blockchain} + /> </Drawer> ); } diff --git a/packages/website/ts/components/ui/identicon.tsx b/packages/website/ts/components/ui/identicon.tsx index 83c86a144..30df995c8 100644 --- a/packages/website/ts/components/ui/identicon.tsx +++ b/packages/website/ts/components/ui/identicon.tsx @@ -1,7 +1,9 @@ import blockies = require('blockies'); import * as _ from 'lodash'; import * as React from 'react'; -import { constants } from 'ts/utils/constants'; + +import { Image } from 'ts/components/ui/image'; +import { colors } from 'ts/style/colors'; interface IdenticonProps { address: string; @@ -16,14 +18,9 @@ export class Identicon extends React.Component<IdenticonProps, IdenticonState> { style: {}, }; public render(): React.ReactNode { - let address = this.props.address; - if (_.isEmpty(address)) { - address = constants.NULL_ADDRESS; - } + const address = this.props.address; const diameter = this.props.diameter; - const icon = blockies({ - seed: address.toLowerCase(), - }); + const radius = diameter / 2; return ( <div className="circle mx-auto relative transitionFix" @@ -34,14 +31,19 @@ export class Identicon extends React.Component<IdenticonProps, IdenticonState> { ...this.props.style, }} > - <img - src={icon.toDataURL()} - style={{ - width: diameter, - height: diameter, - imageRendering: 'pixelated', - }} - /> + {!_.isEmpty(address) ? ( + <Image + src={blockies({ + seed: address.toLowerCase(), + }).toDataURL()} + height={diameter} + width={diameter} + /> + ) : ( + <svg height={diameter} width={diameter}> + <circle cx={radius} cy={radius} r={radius} fill={colors.grey200} /> + </svg> + )} </div> ); } diff --git a/packages/website/ts/components/ui/image.tsx b/packages/website/ts/components/ui/image.tsx index 0958d2e5e..369dc8b7e 100644 --- a/packages/website/ts/components/ui/image.tsx +++ b/packages/website/ts/components/ui/image.tsx @@ -5,7 +5,8 @@ export interface ImageProps { className?: string; src?: string; fallbackSrc?: string; - height?: string; + height?: string | number; + width?: string | number; } interface ImageState { imageLoadFailed: boolean; @@ -26,6 +27,7 @@ export class Image extends React.Component<ImageProps, ImageState> { onError={this._onError.bind(this)} src={src} height={this.props.height} + width={this.props.width} /> ); } diff --git a/packages/website/ts/components/wallet/wallet.tsx b/packages/website/ts/components/wallet/wallet.tsx index 3a6d9942d..ac2fe0d31 100644 --- a/packages/website/ts/components/wallet/wallet.tsx +++ b/packages/website/ts/components/wallet/wallet.tsx @@ -1,9 +1,15 @@ -import { EtherscanLinkSuffixes, Styles, utils as sharedUtils } from '@0xproject/react-shared'; +import { + constants as sharedConstants, + EtherscanLinkSuffixes, + Styles, + 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'; @@ -23,7 +29,6 @@ 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 { zIndex } from 'ts/style/z_index'; import { BlockchainErrs, ProviderType, @@ -35,6 +40,7 @@ import { TokenStateByAddress, WebsitePaths, } from 'ts/types'; +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'; @@ -59,6 +65,7 @@ export interface WalletProps { onAddToken: () => void; onRemoveToken: () => void; refetchTokenStateAsync: (tokenAddress: string) => Promise<void>; + style: React.CSSProperties; } interface WalletState { @@ -79,7 +86,6 @@ interface AccessoryItemConfig { const styles: Styles = { root: { width: '100%', - zIndex: zIndex.aboveOverlay, position: 'relative', }, footerItemInnerDiv: { @@ -134,6 +140,9 @@ const NO_ALLOWANCE_TOGGLE_SPACE_WIDTH = 56; const ACCOUNT_PATH = `${WebsitePaths.Portal}/account`; export class Wallet extends React.Component<WalletProps, WalletState> { + public static defaultProps = { + style: {}, + }; constructor(props: WalletProps) { super(props); this.state = { @@ -141,11 +150,10 @@ export class Wallet extends React.Component<WalletProps, WalletState> { isHoveringSidebar: false, }; } - public render(): React.ReactNode { const isBlockchainLoaded = this.props.blockchainIsLoaded && this.props.blockchainErr === BlockchainErrs.NoError; return ( - <Island className="flex flex-column wallet" style={styles.root}> + <Island className="flex flex-column wallet" style={{ ...styles.root, ...this.props.style }}> {isBlockchainLoaded ? this._renderLoadedRows() : this._renderLoadingRows()} </Island> ); @@ -269,7 +277,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> { <ListItem primaryText={ <div className="flex right" style={styles.manageYourWalletText}> - {'manage your wallet'} + manage your wallet </div> // https://github.com/palantir/tslint-react/issues/140 // tslint:disable-next-line:jsx-curly-spacing @@ -488,18 +496,26 @@ export class Wallet extends React.Component<WalletProps, WalletState> { } } const onClick = isWrappedEtherDirectionOpen - ? this._closeWrappedEtherActionRow.bind(this) + ? this._closeWrappedEtherActionRow.bind(this, wrappedEtherDirection) : this._openWrappedEtherActionRow.bind(this, wrappedEtherDirection); return ( <IconButton iconName={buttonIconName} labelText={buttonLabel} onClick={onClick} color={colors.mediumBlue} /> ); } private _openWrappedEtherActionRow(wrappedEtherDirection: Side): void { + const networkName = sharedConstants.NETWORK_NAME_BY_ID[this.props.networkId]; + const action = + wrappedEtherDirection === Side.Deposit ? 'Wallet - Wrap ETH Opened' : 'Wallet - Unwrap WETH Opened'; + analytics.logEvent('Portal', action, networkName); this.setState({ wrappedEtherDirection, }); } - private _closeWrappedEtherActionRow(): void { + private _closeWrappedEtherActionRow(wrappedEtherDirection: Side): void { + const networkName = sharedConstants.NETWORK_NAME_BY_ID[this.props.networkId]; + const action = + wrappedEtherDirection === Side.Deposit ? 'Wallet - Wrap ETH Closed' : 'Wallet - Unwrap WETH Closed'; + analytics.logEvent('Portal', action, networkName); this.setState({ wrappedEtherDirection: undefined, }); diff --git a/packages/website/ts/components/wallet/wrap_ether_item.tsx b/packages/website/ts/components/wallet/wrap_ether_item.tsx index f65257142..d6135ce4d 100644 --- a/packages/website/ts/components/wallet/wrap_ether_item.tsx +++ b/packages/website/ts/components/wallet/wrap_ether_item.tsx @@ -1,4 +1,4 @@ -import { Styles } from '@0xproject/react-shared'; +import { constants as sharedConstants, Styles } from '@0xproject/react-shared'; import { BigNumber, logUtils } from '@0xproject/utils'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; import * as _ from 'lodash'; @@ -11,6 +11,7 @@ import { TokenAmountInput } from 'ts/components/inputs/token_amount_input'; import { Dispatcher } from 'ts/redux/dispatcher'; import { colors } from 'ts/style/colors'; import { BlockchainCallErrs, Side, Token } from 'ts/types'; +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'; @@ -186,6 +187,7 @@ export class WrapEtherItem extends React.Component<WrapEtherItemProps, WrapEther this.setState({ isEthConversionHappening: true, }); + const networkName = sharedConstants.NETWORK_NAME_BY_ID[this.props.networkId]; try { const etherToken = this.props.etherToken; const amountToConvert = this.state.currentInputAmount; @@ -193,10 +195,12 @@ export class WrapEtherItem extends React.Component<WrapEtherItemProps, WrapEther await this.props.blockchain.convertEthToWrappedEthTokensAsync(etherToken.address, amountToConvert); const ethAmount = Web3Wrapper.toUnitAmount(amountToConvert, constants.DECIMAL_PLACES_ETH); this.props.dispatcher.showFlashMessage(`Successfully wrapped ${ethAmount.toString()} ETH to WETH`); + analytics.logEvent('Portal', 'Wrap ETH Successfully', networkName); } else { await this.props.blockchain.convertWrappedEthTokensToEthAsync(etherToken.address, amountToConvert); const tokenAmount = Web3Wrapper.toUnitAmount(amountToConvert, etherToken.decimals); this.props.dispatcher.showFlashMessage(`Successfully unwrapped ${tokenAmount.toString()} WETH to ETH`); + analytics.logEvent('Portal', 'Unwrap WETH Successfully', networkName); } await this.props.refetchEthTokenStateAsync(); this.props.onConversionSuccessful(); @@ -207,11 +211,13 @@ export class WrapEtherItem extends React.Component<WrapEtherItemProps, WrapEther } else if (!utils.didUserDenyWeb3Request(errMsg)) { logUtils.log(`Unexpected error encountered: ${err}`); logUtils.log(err.stack); - const errorMsg = - this.props.direction === Side.Deposit - ? 'Failed to wrap your ETH. Please try again.' - : 'Failed to unwrap your WETH. Please try again.'; - this.props.dispatcher.showFlashMessage(errorMsg); + if (this.props.direction === Side.Deposit) { + this.props.dispatcher.showFlashMessage('Failed to wrap your ETH. Please try again.'); + analytics.logEvent('Portal', 'Wrap ETH Failed', networkName); + } else { + this.props.dispatcher.showFlashMessage('Failed to unwrap your WETH. Please try again.'); + analytics.logEvent('Portal', 'Unwrap WETH Failed', networkName); + } await errorReporter.reportAsync(err); } } diff --git a/packages/website/ts/containers/portal.ts b/packages/website/ts/containers/portal.ts index 5876e65f5..6747cdf4e 100644 --- a/packages/website/ts/containers/portal.ts +++ b/packages/website/ts/containers/portal.ts @@ -28,6 +28,8 @@ interface ConnectedState { userSuppliedOrderCache: Order; flashMessage?: string | React.ReactNode; translate: Translate; + isPortalOnboardingShowing: boolean; + portalOnboardingStep: number; } interface ConnectedDispatch { @@ -76,6 +78,8 @@ const mapStateToProps = (state: State, _ownProps: PortalComponentProps): Connect userSuppliedOrderCache: state.userSuppliedOrderCache, flashMessage: state.flashMessage, translate: state.translate, + isPortalOnboardingShowing: state.isPortalOnboardingShowing, + portalOnboardingStep: state.portalOnboardingStep, }; }; diff --git a/packages/website/ts/containers/portal_onboarding_flow.ts b/packages/website/ts/containers/portal_onboarding_flow.ts index 746adf0ba..ba2b8f512 100644 --- a/packages/website/ts/containers/portal_onboarding_flow.ts +++ b/packages/website/ts/containers/portal_onboarding_flow.ts @@ -15,6 +15,7 @@ interface PortalOnboardingFlowProps { } interface ConnectedState { + networkId: number; stepIndex: number; isRunning: boolean; userAddress: string; @@ -32,6 +33,7 @@ interface ConnectedDispatch { } const mapStateToProps = (state: State, _ownProps: PortalOnboardingFlowProps): ConnectedState => ({ + networkId: state.networkId, stepIndex: state.portalOnboardingStep, isRunning: state.isPortalOnboardingShowing, userAddress: state.userAddress, diff --git a/packages/website/ts/utils/utils.ts b/packages/website/ts/utils/utils.ts index 414361c1b..0bd3dbcfa 100644 --- a/packages/website/ts/utils/utils.ts +++ b/packages/website/ts/utils/utils.ts @@ -190,6 +190,25 @@ export const utils = { const truncatedAddress = `${address.substring(0, 6)}...${address.substr(-4)}`; // 0x3d5a...b287 return truncatedAddress; }, + getReadableAccountState( + isBlockchainReady: boolean, + providerType: ProviderType, + injectedProviderName: string, + userAddress?: string, + ): string { + const isAddressAvailable = !_.isUndefined(userAddress) && !_.isEmpty(userAddress); + const isExternallyInjectedProvider = utils.isExternallyInjected(providerType, injectedProviderName); + if (!isBlockchainReady) { + return 'Loading account'; + } else if (isAddressAvailable) { + return utils.getAddressBeginAndEnd(userAddress); + // tslint:disable-next-line: prefer-conditional-expression + } else if (isExternallyInjectedProvider) { + return 'Account locked'; + } else { + return 'No wallet detected'; + } + }, hasUniqueNameAndSymbol(tokens: Token[], token: Token): boolean { if (token.isRegistered) { return true; // Since it's registered, it is the canonical token |