diff options
Diffstat (limited to 'packages/website/ts')
49 files changed, 425 insertions, 803 deletions
diff --git a/packages/website/ts/blockchain.ts b/packages/website/ts/blockchain.ts index fde134b18..88461f8a9 100644 --- a/packages/website/ts/blockchain.ts +++ b/packages/website/ts/blockchain.ts @@ -15,8 +15,9 @@ import { ledgerEthereumBrowserClientFactoryAsync, LedgerSubprovider, RedundantSubprovider, + RPCSubprovider, SignerSubprovider, - Subprovider, + Web3ProviderEngine, } from '@0xproject/subproviders'; import { BlockParam, @@ -60,9 +61,7 @@ import { configs } from 'ts/utils/configs'; import { constants } from 'ts/utils/constants'; import { errorReporter } from 'ts/utils/error_reporter'; import { utils } from 'ts/utils/utils'; -import ProviderEngine = require('web3-provider-engine'); import FilterSubprovider = require('web3-provider-engine/subproviders/filters'); -import RpcSubprovider = require('web3-provider-engine/subproviders/rpc'); import * as MintableArtifacts from '../contracts/Mintable.json'; @@ -84,11 +83,11 @@ export class Blockchain { public networkId: number; public nodeVersion: string; private _contractWrappers: ContractWrappers; - private _dispatcher: Dispatcher; + private readonly _dispatcher: Dispatcher; private _web3Wrapper?: Web3Wrapper; private _blockchainWatcher?: BlockchainWatcher; private _injectedProviderObservable?: InjectedProviderObservable; - private _injectedProviderUpdateHandler: (update: InjectedProviderUpdate) => Promise<void>; + private readonly _injectedProviderUpdateHandler: (update: InjectedProviderUpdate) => Promise<void>; private _userAddressIfExists: string; private _ledgerSubprovider: LedgerSubprovider; private _defaultGasPrice: BigNumber; @@ -126,7 +125,11 @@ export class Blockchain { let networkIdIfExists: number; if (!_.isUndefined(injectedWeb3IfExists)) { try { - networkIdIfExists = _.parseInt(await promisify<string>(injectedWeb3IfExists.version.getNetwork)()); + networkIdIfExists = _.parseInt( + await promisify<string>( + injectedWeb3IfExists.version.getNetwork.bind(injectedWeb3IfExists.version), + )(), + ); } catch (err) { // Ignore error and proceed with networkId undefined } @@ -148,7 +151,7 @@ export class Blockchain { if (!isU2FSupported) { throw new Error('Cannot update providerType to LEDGER without U2F support'); } - const provider = new ProviderEngine(); + const provider = new Web3ProviderEngine(); const ledgerWalletConfigs = { networkId: networkIdIfExists, ledgerEthereumClientFactoryAsync: ledgerEthereumBrowserClientFactoryAsync, @@ -157,25 +160,21 @@ export class Blockchain { provider.addProvider(ledgerSubprovider); provider.addProvider(new FilterSubprovider()); const rpcSubproviders = _.map(configs.PUBLIC_NODE_URLS_BY_NETWORK_ID[networkIdIfExists], publicNodeUrl => { - return new RpcSubprovider({ - rpcUrl: publicNodeUrl, - }); + return new RPCSubprovider(publicNodeUrl); }); - provider.addProvider(new RedundantSubprovider(rpcSubproviders as Subprovider[])); + provider.addProvider(new RedundantSubprovider(rpcSubproviders)); 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. - const provider = new ProviderEngine(); + const provider = new Web3ProviderEngine(); provider.addProvider(new SignerSubprovider(injectedWeb3.currentProvider)); provider.addProvider(new FilterSubprovider()); const rpcSubproviders = _.map(publicNodeUrlsIfExistsForNetworkId, publicNodeUrl => { - return new RpcSubprovider({ - rpcUrl: publicNodeUrl, - }); + return new RPCSubprovider(publicNodeUrl); }); - provider.addProvider(new RedundantSubprovider(rpcSubproviders as Subprovider[])); + provider.addProvider(new RedundantSubprovider(rpcSubproviders)); provider.start(); return [provider, undefined]; } else if (doesInjectedWeb3Exist) { @@ -185,15 +184,13 @@ export class Blockchain { // 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. - const provider = new ProviderEngine(); + const provider = new Web3ProviderEngine(); provider.addProvider(new FilterSubprovider()); const networkId = constants.NETWORK_ID_MAINNET; const rpcSubproviders = _.map(configs.PUBLIC_NODE_URLS_BY_NETWORK_ID[networkId], publicNodeUrl => { - return new RpcSubprovider({ - rpcUrl: publicNodeUrl, - }); + return new RPCSubprovider(publicNodeUrl); }); - provider.addProvider(new RedundantSubprovider(rpcSubproviders as Subprovider[])); + provider.addProvider(new RedundantSubprovider(rpcSubproviders)); provider.start(); return [provider, undefined]; } @@ -537,7 +534,9 @@ export class Blockchain { } public destroy(): void { this._blockchainWatcher.destroy(); - this._injectedProviderObservable.unsubscribe(this._injectedProviderUpdateHandler); + if (this._injectedProviderObservable) { + this._injectedProviderObservable.unsubscribe(this._injectedProviderUpdateHandler); + } this._stopWatchingExchangeLogFillEvents(); this._stopWatchingGasPrice(); } @@ -569,7 +568,7 @@ export class Blockchain { configs.DEFAULT_TRACKED_TOKEN_SYMBOLS, )}) not found in tokenRegistry: ${JSON.stringify(tokenRegistryTokens)}`, ); - await errorReporter.reportAsync(err); + errorReporter.report(err); return; } if (_.isEmpty(trackedTokensByAddress)) { @@ -680,8 +679,7 @@ export class Blockchain { // Note: it's not entirely clear from the documentation which // errors will be thrown by `watch`. For now, let's log the error // to rollbar and stop watching when one occurs - // tslint:disable-next-line:no-floating-promises - errorReporter.reportAsync(err); // fire and forget + errorReporter.report(err); // fire and forget return; } else { const decodedLog = decodedLogEvent.log; @@ -767,9 +765,13 @@ export class Blockchain { this._contractWrappers.exchange.unsubscribeAll(); } private async _getTokenRegistryTokensByAddressAsync(): Promise<TokenByAddress> { - utils.assert(!_.isUndefined(this._contractWrappers), 'ContractWrappers must be instantiated.'); - const tokenRegistryTokens = await this._contractWrappers.tokenRegistry.getTokensAsync(); - + let tokenRegistryTokens; + if (this.networkId === constants.NETWORK_ID_MAINNET) { + tokenRegistryTokens = await backendClient.getTokenInfosAsync(); + } else { + utils.assert(!_.isUndefined(this._contractWrappers), 'ContractWrappers must be instantiated.'); + tokenRegistryTokens = await this._contractWrappers.tokenRegistry.getTokensAsync(); + } const tokenByAddress: TokenByAddress = {}; _.each(tokenRegistryTokens, (t: ZeroExToken) => { // HACK: For now we have a hard-coded list of iconUrls for the dummyTokens @@ -789,7 +791,7 @@ export class Blockchain { return tokenByAddress; } private async _onPageLoadInitFireAndForgetAsync(): Promise<void> { - await utils.onPageLoadAsync(); // wait for page to load + await utils.onPageLoadPromise; // wait for page to load const networkIdIfExists = await Blockchain._getInjectedWeb3ProviderNetworkIdIfExistsAsync(); this.networkId = !_.isUndefined(networkIdIfExists) ? networkIdIfExists : constants.NETWORK_ID_MAINNET; const injectedWeb3IfExists = Blockchain._getInjectedWeb3(); @@ -909,7 +911,7 @@ export class Blockchain { if (_.includes(errMsg, 'not been deployed to detected network')) { throw new Error(BlockchainCallErrs.ContractDoesNotExist); } else { - await errorReporter.reportAsync(err); + errorReporter.report(err); throw new Error(BlockchainCallErrs.UnhandledError); } } diff --git a/packages/website/ts/blockchain_watcher.ts b/packages/website/ts/blockchain_watcher.ts index 4b23aa98a..39ed8b08b 100644 --- a/packages/website/ts/blockchain_watcher.ts +++ b/packages/website/ts/blockchain_watcher.ts @@ -4,9 +4,9 @@ import * as _ from 'lodash'; import { Dispatcher } from 'ts/redux/dispatcher'; export class BlockchainWatcher { - private _dispatcher: Dispatcher; - private _web3Wrapper: Web3Wrapper; - private _shouldPollUserAddress: boolean; + private readonly _dispatcher: Dispatcher; + private readonly _web3Wrapper: Web3Wrapper; + private readonly _shouldPollUserAddress: boolean; private _watchBalanceIntervalId: NodeJS.Timer; private _prevUserEtherBalanceInWei?: BigNumber; private _prevUserAddressIfExists: string; diff --git a/packages/website/ts/components/dialogs/u2f_not_supported_dialog.tsx b/packages/website/ts/components/dialogs/u2f_not_supported_dialog.tsx index ce86df856..3ebab03ef 100644 --- a/packages/website/ts/components/dialogs/u2f_not_supported_dialog.tsx +++ b/packages/website/ts/components/dialogs/u2f_not_supported_dialog.tsx @@ -14,9 +14,9 @@ export const U2fNotSupportedDialog = (props: U2fNotSupportedDialogProps) => { <Dialog title="U2F Not Supported" titleStyle={{ fontWeight: 100 }} - actions={[<FlatButton key="u2fNo" label="Ok" onTouchTap={props.onToggleDialog.bind(this)} />]} + actions={[<FlatButton key="u2fNo" label="Ok" onTouchTap={props.onToggleDialog} />]} open={props.isOpen} - onRequestClose={props.onToggleDialog.bind(this)} + onRequestClose={props.onToggleDialog} autoScrollBodyContent={true} > <div className="pt2" style={{ color: colors.grey700 }}> diff --git a/packages/website/ts/components/eth_weth_conversion_button.tsx b/packages/website/ts/components/eth_weth_conversion_button.tsx index 4b91a2ebd..b0091a1c1 100644 --- a/packages/website/ts/components/eth_weth_conversion_button.tsx +++ b/packages/website/ts/components/eth_weth_conversion_button.tsx @@ -37,7 +37,7 @@ export class EthWethConversionButton extends React.Component< > { public static defaultProps: Partial<EthWethConversionButtonProps> = { isDisabled: false, - onConversionSuccessful: _.noop, + onConversionSuccessful: _.noop.bind(_), }; public constructor(props: EthWethConversionButtonProps) { super(props); @@ -118,7 +118,7 @@ export class EthWethConversionButton extends React.Component< ? 'Failed to wrap your ETH. Please try again.' : 'Failed to unwrap your WETH. Please try again.'; this.props.dispatcher.showFlashMessage(errorMsg); - await errorReporter.reportAsync(err); + errorReporter.report(err); } } this.setState({ diff --git a/packages/website/ts/components/fill_order.tsx b/packages/website/ts/components/fill_order.tsx index 03ba1183d..7da2e0870 100644 --- a/packages/website/ts/components/fill_order.tsx +++ b/packages/website/ts/components/fill_order.tsx @@ -1,5 +1,5 @@ import { getOrderHashHex, isValidSignature } from '@0xproject/order-utils'; -import { colors, constants as sharedConstants } from '@0xproject/react-shared'; +import { colors } from '@0xproject/react-shared'; import { Order as ZeroExOrder } from '@0xproject/types'; import { BigNumber, logUtils } from '@0xproject/utils'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; @@ -506,6 +506,10 @@ export class FillOrder extends React.Component<FillOrderProps, FillOrderState> { await this._checkForUntrackedTokensAndAskToAddAsync(); } + private _trackOrderEvent(eventName: string): void { + const parsedOrder = this.state.parsedOrder; + analytics.trackOrderEvent(eventName, parsedOrder); + } private async _onFillOrderClickFireAndForgetAsync(): Promise<void> { if (this.props.blockchainErr !== BlockchainErrs.NoError || _.isEmpty(this.props.userAddress)) { this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true); @@ -552,14 +556,12 @@ export class FillOrder extends React.Component<FillOrderProps, FillOrderState> { }); return; } - const networkName = sharedConstants.NETWORK_NAME_BY_ID[this.props.networkId]; - const eventLabel = `${parsedOrder.metadata.takerToken.symbol}-${networkName}`; try { const orderFilledAmount: BigNumber = await this.props.blockchain.fillOrderAsync( signedOrder, this.props.orderFillAmount, ); - analytics.logEvent('Portal', 'Fill Order Success', eventLabel, parsedOrder.signedOrder.takerTokenAmount); + this._trackOrderEvent('Fill Order Success'); // After fill completes, let's force fetch the token balances this.props.dispatcher.forceTokenStateRefetch(); this.setState({ @@ -573,7 +575,7 @@ export class FillOrder extends React.Component<FillOrderProps, FillOrderState> { this.setState({ isFilling: false, }); - analytics.logEvent('Portal', 'Fill Order Failure', eventLabel, parsedOrder.signedOrder.takerTokenAmount); + this._trackOrderEvent('Fill Order Failure'); const errMsg = `${err}`; if (utils.didUserDenyWeb3Request(errMsg)) { return; @@ -583,7 +585,7 @@ export class FillOrder extends React.Component<FillOrderProps, FillOrderState> { this.setState({ globalErrMsg, }); - await errorReporter.reportAsync(err); + errorReporter.report(err); return; } } @@ -628,8 +630,6 @@ export class FillOrder extends React.Component<FillOrderProps, FillOrderState> { }); return; } - const networkName = sharedConstants.NETWORK_NAME_BY_ID[this.props.networkId]; - const eventLabel = `${parsedOrder.metadata.makerToken.symbol}-${networkName}`; try { await this.props.blockchain.cancelOrderAsync(signedOrder, availableTakerTokenAmount); this.setState({ @@ -638,7 +638,7 @@ export class FillOrder extends React.Component<FillOrderProps, FillOrderState> { globalErrMsg: '', unavailableTakerAmount: takerTokenAmount, }); - analytics.logEvent('Portal', 'Cancel Order Success', eventLabel, parsedOrder.signedOrder.makerTokenAmount); + this._trackOrderEvent('Cancel Order Success'); return; } catch (err) { this.setState({ @@ -648,13 +648,13 @@ export class FillOrder extends React.Component<FillOrderProps, FillOrderState> { if (utils.didUserDenyWeb3Request(errMsg)) { return; } - analytics.logEvent('Portal', 'Cancel Order Failure', eventLabel, parsedOrder.signedOrder.makerTokenAmount); + this._trackOrderEvent('Cancel Order Failure'); globalErrMsg = 'Failed to cancel order, please refresh and try again'; logUtils.log(`${err}`); this.setState({ globalErrMsg, }); - await errorReporter.reportAsync(err); + errorReporter.report(err); return; } } diff --git a/packages/website/ts/components/fill_warning_dialog.tsx b/packages/website/ts/components/fill_warning_dialog.tsx index 83095b1d3..45c492221 100644 --- a/packages/website/ts/components/fill_warning_dialog.tsx +++ b/packages/website/ts/components/fill_warning_dialog.tsx @@ -18,16 +18,16 @@ export const FillWarningDialog = (props: FillWarningDialogProps) => { <FlatButton key="fillWarningCancel" label="Cancel" - onTouchTap={props.onToggleDialog.bind(this, didCancel)} + onTouchTap={() => props.onToggleDialog(didCancel)} // tslint:disable-line:jsx-no-lambda />, <FlatButton key="fillWarningContinue" label="Fill Order" - onTouchTap={props.onToggleDialog.bind(this, !didCancel)} + onTouchTap={() => props.onToggleDialog(!didCancel)} // tslint:disable-line:jsx-no-lambda />, ]} open={props.isOpen} - onRequestClose={props.onToggleDialog.bind(this)} + onRequestClose={() => props.onToggleDialog(didCancel)} // tslint:disable-line:jsx-no-lambda autoScrollBodyContent={true} modal={true} > diff --git a/packages/website/ts/components/forms/subscribe_form.tsx b/packages/website/ts/components/forms/subscribe_form.tsx index 8ef58328e..761db7517 100644 --- a/packages/website/ts/components/forms/subscribe_form.tsx +++ b/packages/website/ts/components/forms/subscribe_form.tsx @@ -6,6 +6,7 @@ import { Button } from 'ts/components/ui/button'; import { Container } from 'ts/components/ui/container'; import { Input } from 'ts/components/ui/input'; import { Text } from 'ts/components/ui/text'; +import { analytics } from 'ts/utils/analytics'; import { backendClient } from 'ts/utils/backend_client'; export interface SubscribeFormProps {} @@ -112,6 +113,9 @@ export class SubscribeForm extends React.Component<SubscribeFormProps, Subscribe try { const response = await backendClient.subscribeToNewsletterAsync(this.state.emailText); const status = response.status === 200 ? SubscribeFormStatus.Success : SubscribeFormStatus.Error; + if (status === SubscribeFormStatus.Success) { + analytics.identify(this.state.emailText, 'email'); + } this.setState({ status, emailText: '' }); } catch (error) { this._setStatus(SubscribeFormStatus.Error); diff --git a/packages/website/ts/components/generate_order/asset_picker.tsx b/packages/website/ts/components/generate_order/asset_picker.tsx index 5eada37b6..2dca3483f 100644 --- a/packages/website/ts/components/generate_order/asset_picker.tsx +++ b/packages/website/ts/components/generate_order/asset_picker.tsx @@ -46,7 +46,7 @@ export class AssetPicker extends React.Component<AssetPickerProps, AssetPickerSt public static defaultProps: Partial<AssetPickerProps> = { tokenVisibility: TokenVisibility.ALL, }; - private _dialogConfigsByAssetView: { [assetView: string]: DialogConfigs }; + private readonly _dialogConfigsByAssetView: { [assetView: string]: DialogConfigs }; constructor(props: AssetPickerProps) { super(props); this.state = { diff --git a/packages/website/ts/components/generate_order/generate_order_form.tsx b/packages/website/ts/components/generate_order/generate_order_form.tsx index d26b5c3fa..72efab033 100644 --- a/packages/website/ts/components/generate_order/generate_order_form.tsx +++ b/packages/website/ts/components/generate_order/generate_order_form.tsx @@ -1,6 +1,6 @@ import { generatePseudoRandomSalt, getOrderHashHex } from '@0xproject/order-utils'; -import { colors, constants as sharedConstants } from '@0xproject/react-shared'; -import { ECSignature, Order } from '@0xproject/types'; +import { colors } from '@0xproject/react-shared'; +import { ECSignature, Order as ZeroExOrder } from '@0xproject/types'; import { BigNumber, logUtils } from '@0xproject/utils'; import * as _ from 'lodash'; import Dialog from 'material-ui/Dialog'; @@ -20,7 +20,7 @@ import { SwapIcon } from 'ts/components/ui/swap_icon'; import { Dispatcher } from 'ts/redux/dispatcher'; import { portalOrderSchema } from 'ts/schemas/portal_order_schema'; import { validator } from 'ts/schemas/validator'; -import { AlertTypes, BlockchainErrs, HashData, Side, SideToAssetToken, Token, TokenByAddress } from 'ts/types'; +import { AlertTypes, BlockchainErrs, HashData, Order, Side, SideToAssetToken, Token, TokenByAddress } from 'ts/types'; import { analytics } from 'ts/utils/analytics'; import { constants } from 'ts/utils/constants'; import { errorReporter } from 'ts/utils/error_reporter'; @@ -254,7 +254,8 @@ export class GenerateOrderForm extends React.Component<GenerateOrderFormProps, G userAddressIfExists, debitToken.address, ); - const receiveAmount = this.props.sideToAssetToken[Side.Receive].amount; + const receiveToken = this.props.sideToAssetToken[Side.Receive]; + const receiveAmount = receiveToken.amount; if ( !_.isUndefined(debitToken.amount) && !_.isUndefined(receiveAmount) && @@ -264,24 +265,28 @@ export class GenerateOrderForm extends React.Component<GenerateOrderFormProps, G debitBalance.gte(debitToken.amount) && debitAllowance.gte(debitToken.amount) ) { - const didSignSuccessfully = await this._signTransactionAsync(); - if (didSignSuccessfully) { - const networkName = sharedConstants.NETWORK_NAME_BY_ID[this.props.networkId]; - const eventLabel = `${this.props.tokenByAddress[debitToken.address].symbol}-${networkName}`; - analytics.logEvent('Portal', 'Sign Order Success', eventLabel, debitToken.amount.toNumber()); + const signedOrder = await this._signTransactionAsync(); + const doesSignedOrderExist = !_.isUndefined(signedOrder); + if (doesSignedOrderExist) { + analytics.trackOrderEvent('Sign Order Success', signedOrder); this.setState({ globalErrMsg: '', shouldShowIncompleteErrs: false, }); } - return didSignSuccessfully; + return doesSignedOrderExist; } else { let globalErrMsg = 'You must fix the above errors in order to generate a valid order'; if (this.props.userAddress === '') { globalErrMsg = 'You must enable wallet communication'; this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true); } - analytics.logEvent('Portal', 'Sign Order Failure', globalErrMsg); + analytics.track('Sign Order Failure', { + makerTokenAmount: debitToken.amount.toString(), + makerToken: this.props.tokenByAddress[debitToken.address].symbol, + takerTokenAmount: receiveToken.amount.toString(), + takerToken: this.props.tokenByAddress[receiveToken.address].symbol, + }); this.setState({ globalErrMsg, shouldShowIncompleteErrs: true, @@ -289,7 +294,7 @@ export class GenerateOrderForm extends React.Component<GenerateOrderFormProps, G return false; } } - private async _signTransactionAsync(): Promise<boolean> { + private async _signTransactionAsync(): Promise<Order | undefined> { this.setState({ signingState: SigningState.SIGNING, }); @@ -299,11 +304,11 @@ export class GenerateOrderForm extends React.Component<GenerateOrderFormProps, G this.setState({ signingState: SigningState.UNSIGNED, }); - return false; + return undefined; } const hashData = this.props.hashData; - const zeroExOrder: Order = { + const zeroExOrder: ZeroExOrder = { exchangeContractAddress: exchangeContractAddr, expirationUnixTimestampSec: hashData.orderExpiryTimestamp, feeRecipient: hashData.feeRecipientAddress, @@ -320,9 +325,10 @@ export class GenerateOrderForm extends React.Component<GenerateOrderFormProps, G const orderHash = getOrderHashHex(zeroExOrder); let globalErrMsg = ''; + let order; try { const ecSignature = await this.props.blockchain.signOrderHashAsync(orderHash); - const order = utils.generateOrder( + order = utils.generateOrder( exchangeContractAddr, this.props.sideToAssetToken, hashData.orderExpiryTimestamp, @@ -349,14 +355,14 @@ export class GenerateOrderForm extends React.Component<GenerateOrderFormProps, G globalErrMsg = 'An unexpected error occured. Please try refreshing the page'; logUtils.log(`Unexpected error occured: ${err}`); logUtils.log(err.stack); - await errorReporter.reportAsync(err); + errorReporter.report(err); } } this.setState({ signingState: globalErrMsg === '' ? SigningState.SIGNED : SigningState.UNSIGNED, globalErrMsg, }); - return globalErrMsg === ''; + return order; } private _updateOrderAddress(address?: string): void { if (!_.isUndefined(address)) { diff --git a/packages/website/ts/components/inputs/allowance_toggle.tsx b/packages/website/ts/components/inputs/allowance_toggle.tsx index 0d5995696..05dce134a 100644 --- a/packages/website/ts/components/inputs/allowance_toggle.tsx +++ b/packages/website/ts/components/inputs/allowance_toggle.tsx @@ -1,4 +1,4 @@ -import { constants as sharedConstants, Styles } from '@0xproject/react-shared'; +import { Styles } from '@0xproject/react-shared'; import { BigNumber, logUtils } from '@0xproject/utils'; import * as _ from 'lodash'; import Toggle from 'material-ui/Toggle'; @@ -57,7 +57,7 @@ const styles: Styles = { export class AllowanceToggle extends React.Component<AllowanceToggleProps, AllowanceToggleState> { public static defaultProps = { - onErrorOccurred: _.noop, + onErrorOccurred: _.noop.bind(_), isDisabled: false, }; constructor(props: AllowanceToggleProps) { @@ -111,14 +111,16 @@ export class AllowanceToggle extends React.Component<AllowanceToggleProps, Allow if (!this._isAllowanceSet()) { newAllowanceAmountInBaseUnits = DEFAULT_ALLOWANCE_AMOUNT_IN_BASE_UNITS; } - const networkName = sharedConstants.NETWORK_NAME_BY_ID[this.props.networkId]; - const eventLabel = `${this.props.token.symbol}-${networkName}`; + const logData = { + tokenSymbol: this.props.token.symbol, + newAllowance: newAllowanceAmountInBaseUnits.toNumber(), + }; try { await this.props.blockchain.setProxyAllowanceAsync(this.props.token, newAllowanceAmountInBaseUnits); - analytics.logEvent('Portal', 'Set Allowance Success', eventLabel, newAllowanceAmountInBaseUnits.toNumber()); + analytics.track('Set Allowances Success', logData); await this.props.refetchTokenStateAsync(); } catch (err) { - analytics.logEvent('Portal', 'Set Allowance Failure', eventLabel, newAllowanceAmountInBaseUnits.toNumber()); + analytics.track('Set Allowance Failure', logData); this.setState({ isSpinnerVisible: false, }); @@ -129,7 +131,7 @@ export class AllowanceToggle extends React.Component<AllowanceToggleProps, Allow logUtils.log(`Unexpected error encountered: ${err}`); logUtils.log(err.stack); this.props.onErrorOccurred(BalanceErrs.allowanceSettingFailed); - await errorReporter.reportAsync(err); + errorReporter.report(err); } } private _isAllowanceSet(): boolean { diff --git a/packages/website/ts/components/inputs/balance_bounded_input.tsx b/packages/website/ts/components/inputs/balance_bounded_input.tsx index f23beb436..eb01e3ea6 100644 --- a/packages/website/ts/components/inputs/balance_bounded_input.tsx +++ b/packages/website/ts/components/inputs/balance_bounded_input.tsx @@ -35,7 +35,7 @@ export class BalanceBoundedInput extends React.Component<BalanceBoundedInputProp isDisabled: false, shouldShowErrs: true, hintText: 'amount', - onErrorMsgChange: _.noop, + onErrorMsgChange: _.noop.bind(_), shouldShowUnderline: true, }; constructor(props: BalanceBoundedInputProps) { @@ -125,7 +125,7 @@ export class BalanceBoundedInput extends React.Component<BalanceBoundedInputProp const errMsg = _.isUndefined(this.props.validate) ? undefined : this.props.validate(amount); return errMsg; } - private _setAmountState(amount: string, balance: BigNumber, callback: () => void = _.noop): void { + private _setAmountState(amount: string, balance: BigNumber, callback: () => void = _.noop.bind(_)): void { const errorMsg = this._validate(amount, balance); this.props.onErrorMsgChange(errorMsg); this.setState( diff --git a/packages/website/ts/components/inputs/expiration_input.tsx b/packages/website/ts/components/inputs/expiration_input.tsx index 79dd2f568..5417ce715 100644 --- a/packages/website/ts/components/inputs/expiration_input.tsx +++ b/packages/website/ts/components/inputs/expiration_input.tsx @@ -17,7 +17,7 @@ interface ExpirationInputState { } export class ExpirationInput extends React.Component<ExpirationInputProps, ExpirationInputState> { - private _earliestPickableMoment: moment.Moment; + private readonly _earliestPickableMoment: moment.Moment; constructor(props: ExpirationInputProps) { super(props); // Set the earliest pickable date to today at 00:00, so users can only pick the current or later dates diff --git a/packages/website/ts/components/legacy_portal/legacy_portal.tsx b/packages/website/ts/components/legacy_portal/legacy_portal.tsx deleted file mode 100644 index c85d97207..000000000 --- a/packages/website/ts/components/legacy_portal/legacy_portal.tsx +++ /dev/null @@ -1,339 +0,0 @@ -import { colors } from '@0xproject/react-shared'; -import { BigNumber } from '@0xproject/utils'; -import * as _ from 'lodash'; -import CircularProgress from 'material-ui/CircularProgress'; -import Paper from 'material-ui/Paper'; -import * as React from 'react'; -import * as DocumentTitle from 'react-document-title'; -import { Route, Switch } from 'react-router-dom'; -import { Blockchain } from 'ts/blockchain'; -import { BlockchainErrDialog } from 'ts/components/dialogs/blockchain_err_dialog'; -import { LedgerConfigDialog } from 'ts/components/dialogs/ledger_config_dialog'; -import { PortalDisclaimerDialog } from 'ts/components/dialogs/portal_disclaimer_dialog'; -import { WrappedEthSectionNoticeDialog } from 'ts/components/dialogs/wrapped_eth_section_notice_dialog'; -import { EthWrappers } from 'ts/components/eth_wrappers'; -import { FillOrder } from 'ts/components/fill_order'; -import { Footer } from 'ts/components/footer'; -import { LegacyPortalMenu } from 'ts/components/legacy_portal/legacy_portal_menu'; -import { TokenBalances } from 'ts/components/token_balances'; -import { TopBar } from 'ts/components/top_bar/top_bar'; -import { TradeHistory } from 'ts/components/trade_history/trade_history'; -import { FlashMessage } from 'ts/components/ui/flash_message'; -import { 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 { constants } from 'ts/utils/constants'; -import { orderParser } from 'ts/utils/order_parser'; -import { Translate } from 'ts/utils/translate'; -import { utils } from 'ts/utils/utils'; - -const THROTTLE_TIMEOUT = 100; - -export interface LegacyPortalProps { - blockchainErr: BlockchainErrs; - blockchainIsLoaded: boolean; - dispatcher: Dispatcher; - hashData: HashData; - injectedProviderName: string; - networkId: number; - nodeVersion: string; - orderFillAmount: BigNumber; - providerType: ProviderType; - screenWidth: ScreenWidths; - tokenByAddress: TokenByAddress; - userEtherBalanceInWei?: BigNumber; - userAddress: string; - shouldBlockchainErrDialogBeOpen: boolean; - userSuppliedOrderCache: Order; - location: Location; - flashMessage?: string | React.ReactNode; - lastForceTokenStateRefetch: number; - translate: Translate; -} - -interface LegacyPortalState { - prevNetworkId: number; - prevNodeVersion: string; - prevUserAddress: string; - prevPathname: string; - isDisclaimerDialogOpen: boolean; - isWethNoticeDialogOpen: boolean; - isLedgerDialogOpen: boolean; -} - -export class LegacyPortal extends React.Component<LegacyPortalProps, LegacyPortalState> { - private _blockchain: Blockchain; - private _sharedOrderIfExists: Order; - private _throttledScreenWidthUpdate: () => void; - public static hasAlreadyDismissedWethNotice(): boolean { - const didDismissWethNotice = localStorage.getItemIfExists(constants.LOCAL_STORAGE_KEY_DISMISS_WETH_NOTICE); - const hasAlreadyDismissedWethNotice = !_.isUndefined(didDismissWethNotice) && !_.isEmpty(didDismissWethNotice); - return hasAlreadyDismissedWethNotice; - } - constructor(props: LegacyPortalProps) { - super(props); - this._sharedOrderIfExists = orderParser.parse(window.location.search); - this._throttledScreenWidthUpdate = _.throttle(this._updateScreenWidth.bind(this), THROTTLE_TIMEOUT); - - const isViewingBalances = _.includes(props.location.pathname, `${WebsitePaths.Portal}/balances`); - const hasAlreadyDismissedWethNotice = LegacyPortal.hasAlreadyDismissedWethNotice(); - - const didAcceptPortalDisclaimer = localStorage.getItemIfExists(constants.LOCAL_STORAGE_KEY_ACCEPT_DISCLAIMER); - const hasAcceptedDisclaimer = - !_.isUndefined(didAcceptPortalDisclaimer) && !_.isEmpty(didAcceptPortalDisclaimer); - this.state = { - prevNetworkId: this.props.networkId, - prevNodeVersion: this.props.nodeVersion, - prevUserAddress: this.props.userAddress, - prevPathname: this.props.location.pathname, - isDisclaimerDialogOpen: !hasAcceptedDisclaimer, - isWethNoticeDialogOpen: !hasAlreadyDismissedWethNotice && isViewingBalances, - isLedgerDialogOpen: false, - }; - } - public componentDidMount(): void { - window.addEventListener('resize', this._throttledScreenWidthUpdate); - window.scrollTo(0, 0); - } - public componentWillMount(): void { - this._blockchain = new Blockchain(this.props.dispatcher); - } - public componentWillUnmount(): void { - this._blockchain.destroy(); - window.removeEventListener('resize', this._throttledScreenWidthUpdate); - // We re-set the entire redux state when the portal is unmounted so that when it is re-rendered - // the initialization process always occurs from the same base state. This helps avoid - // initialization inconsistencies (i.e While the portal was unrendered, the user might have - // become disconnected from their backing Ethereum node, changes user accounts, etc...) - this.props.dispatcher.resetState(); - } - public componentWillReceiveProps(nextProps: LegacyPortalProps): void { - if (nextProps.networkId !== this.state.prevNetworkId) { - // tslint:disable-next-line:no-floating-promises - this._blockchain.networkIdUpdatedFireAndForgetAsync(nextProps.networkId); - this.setState({ - prevNetworkId: nextProps.networkId, - }); - } - if (nextProps.userAddress !== this.state.prevUserAddress) { - const newUserAddress = _.isEmpty(nextProps.userAddress) ? undefined : nextProps.userAddress; - // tslint:disable-next-line:no-floating-promises - this._blockchain.userAddressUpdatedFireAndForgetAsync(newUserAddress); - this.setState({ - prevUserAddress: nextProps.userAddress, - }); - } - if (nextProps.nodeVersion !== this.state.prevNodeVersion) { - // tslint:disable-next-line:no-floating-promises - this._blockchain.nodeVersionUpdatedFireAndForgetAsync(nextProps.nodeVersion); - } - if (nextProps.location.pathname !== this.state.prevPathname) { - const isViewingBalances = _.includes(nextProps.location.pathname, `${WebsitePaths.Portal}/balances`); - const hasAlreadyDismissedWethNotice = LegacyPortal.hasAlreadyDismissedWethNotice(); - this.setState({ - prevPathname: nextProps.location.pathname, - isWethNoticeDialogOpen: !hasAlreadyDismissedWethNotice && isViewingBalances, - }); - } - } - public render(): React.ReactNode { - const updateShouldBlockchainErrDialogBeOpen = this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen.bind( - this.props.dispatcher, - ); - const portalStyle: React.CSSProperties = { - minHeight: '100vh', - display: 'flex', - flexDirection: 'column', - justifyContent: 'space-between', - }; - const portalMenuContainerStyle: React.CSSProperties = { - overflow: 'hidden', - backgroundColor: colors.darkestGrey, - color: colors.white, - }; - return ( - <div style={portalStyle}> - <DocumentTitle title="0x Portal DApp" /> - <TopBar - userAddress={this.props.userAddress} - networkId={this.props.networkId} - injectedProviderName={this.props.injectedProviderName} - onToggleLedgerDialog={this.onToggleLedgerDialog.bind(this)} - dispatcher={this.props.dispatcher} - providerType={this.props.providerType} - blockchainIsLoaded={this.props.blockchainIsLoaded} - location={this.props.location} - blockchain={this._blockchain} - translate={this.props.translate} - /> - <div id="portal" className="mx-auto max-width-4" style={{ width: '100%' }}> - <Paper className="mb3 mt2"> - <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> - </div> - )} - </div> - </div> - </div> - </Paper> - <BlockchainErrDialog - blockchain={this._blockchain} - blockchainErr={this.props.blockchainErr} - isOpen={this.props.shouldBlockchainErrDialogBeOpen} - userAddress={this.props.userAddress} - toggleDialogFn={updateShouldBlockchainErrDialogBeOpen} - networkId={this.props.networkId} - /> - <WrappedEthSectionNoticeDialog - isOpen={this.state.isWethNoticeDialogOpen} - onToggleDialog={this._onWethNoticeAccepted.bind(this)} - /> - <PortalDisclaimerDialog - isOpen={this.state.isDisclaimerDialogOpen} - onToggleDialog={this._onPortalDisclaimerAccepted.bind(this)} - /> - <FlashMessage dispatcher={this.props.dispatcher} flashMessage={this.props.flashMessage} /> - <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> - ); - } - public onToggleLedgerDialog(): void { - this.setState({ - isLedgerDialogOpen: !this.state.isLedgerDialogOpen, - }); - } - private _renderEthWrapper(): React.ReactNode { - return ( - <EthWrappers - networkId={this.props.networkId} - blockchain={this._blockchain} - dispatcher={this.props.dispatcher} - tokenByAddress={this.props.tokenByAddress} - userAddress={this.props.userAddress} - userEtherBalanceInWei={this.props.userEtherBalanceInWei} - lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} - /> - ); - } - private _renderTradeHistory(): React.ReactNode { - return ( - <TradeHistory - tokenByAddress={this.props.tokenByAddress} - userAddress={this.props.userAddress} - networkId={this.props.networkId} - /> - ); - } - private _renderTokenBalances(): React.ReactNode { - const trackedTokens = utils.getTrackedTokens(this.props.tokenByAddress); - return ( - <TokenBalances - blockchain={this._blockchain} - blockchainErr={this.props.blockchainErr} - blockchainIsLoaded={this.props.blockchainIsLoaded} - dispatcher={this.props.dispatcher} - screenWidth={this.props.screenWidth} - tokenByAddress={this.props.tokenByAddress} - trackedTokens={trackedTokens} - userAddress={this.props.userAddress} - userEtherBalanceInWei={this.props.userEtherBalanceInWei} - networkId={this.props.networkId} - lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} - /> - ); - } - private _renderFillOrder(_match: any, _location: Location, _history: History): React.ReactNode { - const initialFillOrder = !_.isUndefined(this.props.userSuppliedOrderCache) - ? this.props.userSuppliedOrderCache - : this._sharedOrderIfExists; - return ( - <FillOrder - blockchain={this._blockchain} - blockchainErr={this.props.blockchainErr} - initialOrder={initialFillOrder} - isOrderInUrl={!_.isUndefined(this._sharedOrderIfExists)} - orderFillAmount={this.props.orderFillAmount} - networkId={this.props.networkId} - userAddress={this.props.userAddress} - tokenByAddress={this.props.tokenByAddress} - dispatcher={this.props.dispatcher} - lastForceTokenStateRefetch={this.props.lastForceTokenStateRefetch} - /> - ); - } - private _renderGenerateOrderForm(_match: any, _location: Location, _history: History): React.ReactNode { - return ( - <GenerateOrderForm - blockchain={this._blockchain} - hashData={this.props.hashData} - dispatcher={this.props.dispatcher} - /> - ); - } - private _onPortalDisclaimerAccepted(): void { - localStorage.setItem(constants.LOCAL_STORAGE_KEY_ACCEPT_DISCLAIMER, 'set'); - this.setState({ - isDisclaimerDialogOpen: false, - }); - } - private _onWethNoticeAccepted(): void { - localStorage.setItem(constants.LOCAL_STORAGE_KEY_DISMISS_WETH_NOTICE, 'set'); - this.setState({ - isWethNoticeDialogOpen: false, - }); - } - private _updateScreenWidth(): void { - const newScreenWidth = utils.getScreenWidth(); - this.props.dispatcher.updateScreenWidth(newScreenWidth); - } -} diff --git a/packages/website/ts/components/legacy_portal/legacy_portal_menu.tsx b/packages/website/ts/components/legacy_portal/legacy_portal_menu.tsx deleted file mode 100644 index 1dd164f8b..000000000 --- a/packages/website/ts/components/legacy_portal/legacy_portal_menu.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import * as _ from 'lodash'; -import * as React from 'react'; -import { MenuItem } from 'ts/components/ui/menu_item'; -import { WebsitePaths } from 'ts/types'; - -export interface LegacyPortalMenuProps { - menuItemStyle: React.CSSProperties; - onClick?: () => void; -} - -interface LegacyPortalMenuState {} - -export class LegacyPortalMenu extends React.Component<LegacyPortalMenuProps, LegacyPortalMenuState> { - public static defaultProps: Partial<LegacyPortalMenuProps> = { - onClick: _.noop, - }; - public render(): React.ReactNode { - return ( - <div> - <MenuItem - style={this.props.menuItemStyle} - className="py2" - to={`${WebsitePaths.Portal}`} - onClick={this.props.onClick.bind(this)} - > - {this._renderMenuItemWithIcon('Generate order', 'zmdi-arrow-right-top')} - </MenuItem> - <MenuItem - style={this.props.menuItemStyle} - className="py2" - to={`${WebsitePaths.Portal}/fill`} - onClick={this.props.onClick.bind(this)} - > - {this._renderMenuItemWithIcon('Fill order', 'zmdi-arrow-left-bottom')} - </MenuItem> - <MenuItem - style={this.props.menuItemStyle} - className="py2" - to={`${WebsitePaths.Portal}/balances`} - onClick={this.props.onClick.bind(this)} - > - {this._renderMenuItemWithIcon('Balances', 'zmdi-balance-wallet')} - </MenuItem> - <MenuItem - style={this.props.menuItemStyle} - className="py2" - to={`${WebsitePaths.Portal}/trades`} - onClick={this.props.onClick.bind(this)} - > - {this._renderMenuItemWithIcon('Trade history', 'zmdi-format-list-bulleted')} - </MenuItem> - <MenuItem - style={this.props.menuItemStyle} - className="py2" - to={`${WebsitePaths.Portal}/weth`} - onClick={this.props.onClick.bind(this)} - > - {this._renderMenuItemWithIcon('Wrap ETH', 'zmdi-circle-o')} - </MenuItem> - </div> - ); - } - private _renderMenuItemWithIcon(title: string, iconName: string): React.ReactNode { - return ( - <div className="flex" style={{ fontWeight: 100 }}> - <div className="pr1 pl2"> - <i style={{ fontSize: 20 }} className={`zmdi ${iconName}`} /> - </div> - <div className="pl1">{title}</div> - </div> - ); - } -} diff --git a/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx b/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx index 20a8f0a32..f395674a1 100644 --- a/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx +++ b/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx @@ -1,4 +1,3 @@ -import { constants as sharedConstants } from '@0xproject/react-shared'; import * as _ from 'lodash'; import * as React from 'react'; import { RouteComponentProps, withRouter } from 'react-router'; @@ -225,20 +224,24 @@ class PlainPortalOnboardingFlow extends React.Component<PortalOnboardingFlowProp (this.props.stepIndex === 0 && !this.props.isRunning && this.props.blockchainIsLoaded) || (!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); + analytics.track('Onboarding Started', { + reason: 'automatic', + stepIndex: 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); + analytics.track('Update Onboarding Step', { + 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); + analytics.track('Onboarding Closed', { + stepIndex: this.props.stepIndex, + }); } private _renderZrxAllowanceToggle(): React.ReactNode { const zrxToken = utils.getZrxToken(this.props.tokenByAddress); diff --git a/packages/website/ts/components/order_json.tsx b/packages/website/ts/components/order_json.tsx index 35188c024..cf06f10c8 100644 --- a/packages/website/ts/components/order_json.tsx +++ b/packages/website/ts/components/order_json.tsx @@ -1,5 +1,5 @@ import { ECSignature } from '@0xproject/types'; -import { BigNumber, logUtils } from '@0xproject/utils'; +import { BigNumber, fetchAsync, logUtils } from '@0xproject/utils'; import * as _ from 'lodash'; import Paper from 'material-ui/Paper'; import TextField from 'material-ui/TextField'; @@ -127,7 +127,7 @@ export class OrderJSON extends React.Component<OrderJSONProps, OrderJSONState> { href: this.state.shareLink, method: 'share', }, - _.noop, + _.noop.bind(_), ); } private _shareViaEmailAsync(): void { @@ -148,13 +148,13 @@ You can see and fill it here: ${this.state.shareLink}`); const bitlyRequestUrl = `${constants.URL_BITLY_API}/v3/shorten?access_token=${ configs.BITLY_ACCESS_TOKEN }&longUrl=${longUrl}`; - const response = await fetch(bitlyRequestUrl); + const response = await fetchAsync(bitlyRequestUrl); const responseBody = await response.text(); const bodyObj = JSON.parse(responseBody); if (response.status !== 200 || bodyObj.status_code !== 200) { // TODO: Show error message in UI logUtils.log(`Unexpected status code: ${response.status} -> ${responseBody}`); - await errorReporter.reportAsync(new Error(`Bitly returned non-200: ${JSON.stringify(response)}`)); + errorReporter.report(new Error(`Bitly returned non-200: ${JSON.stringify(response)}`)); return ''; } return bodyObj.data.url; diff --git a/packages/website/ts/components/portal/portal.tsx b/packages/website/ts/components/portal/portal.tsx index f983241fa..1790a9678 100644 --- a/packages/website/ts/components/portal/portal.tsx +++ b/packages/website/ts/components/portal/portal.tsx @@ -1,4 +1,4 @@ -import { colors, constants as sharedConstants } from '@0xproject/react-shared'; +import { colors } from '@0xproject/react-shared'; import { BigNumber } from '@0xproject/utils'; import * as _ from 'lodash'; import * as React from 'react'; @@ -110,8 +110,8 @@ const SIDE_PADDING = 20; export class Portal extends React.Component<PortalProps, PortalState> { private _blockchain: Blockchain; - private _sharedOrderIfExists: Order; - private _throttledScreenWidthUpdate: () => void; + private readonly _sharedOrderIfExists: Order; + private readonly _throttledScreenWidthUpdate: () => void; constructor(props: PortalProps) { super(props); this._sharedOrderIfExists = orderParser.parse(window.location.search); @@ -388,10 +388,11 @@ export class Portal extends React.Component<PortalProps, PortalState> { startOnboarding ); } - private _startOnboarding(): void { - const networkName = sharedConstants.NETWORK_NAME_BY_ID[this.props.networkId]; - analytics.logEvent('Portal', 'Onboarding Started - Manual', networkName, this.props.portalOnboardingStep); + analytics.track('Onboarding Started', { + reason: 'manual', + stepIndex: 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 431cf145b..193dd237a 100644 --- a/packages/website/ts/components/relayer_index/relayer_grid_tile.tsx +++ b/packages/website/ts/components/relayer_index/relayer_grid_tile.tsx @@ -1,4 +1,4 @@ -import { constants as sharedConstants, Styles } from '@0xproject/react-shared'; +import { Styles } from '@0xproject/react-shared'; import * as _ from 'lodash'; import { GridTile as PlainGridTile } from 'material-ui/GridList'; import * as React from 'react'; @@ -64,10 +64,10 @@ 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 onClick = () => { - analytics.logEvent('Portal', 'Relayer Click', eventLabel); + analytics.track('Relayer Click', { + name: props.relayerInfo.name, + }); utils.openUrl(link); }; const headerImageUrl = props.relayerInfo.logoImgUrl; 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 c48b672e9..f3787bd27 100644 --- a/packages/website/ts/components/relayer_index/relayer_top_tokens.tsx +++ b/packages/website/ts/components/relayer_index/relayer_top_tokens.tsx @@ -1,9 +1,4 @@ -import { - colors, - constants as sharedConstants, - EtherscanLinkSuffixes, - utils as sharedUtils, -} from '@0xproject/react-shared'; +import { colors, EtherscanLinkSuffixes, utils as sharedUtils } from '@0xproject/react-shared'; import * as _ from 'lodash'; import * as React from 'react'; @@ -46,11 +41,11 @@ class TokenLink extends React.Component<TokenLinkProps, TokenLinkState> { }; } public render(): React.ReactNode { - 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); + analytics.track('Token Click', { + tokenSymbol: this.props.tokenInfo.symbol, + }); const tokenLink = this._tokenLinkFromToken(this.props.tokenInfo, this.props.networkId); utils.openUrl(tokenLink); }; diff --git a/packages/website/ts/components/send_button.tsx b/packages/website/ts/components/send_button.tsx index 8486dbd8b..ac55d430b 100644 --- a/packages/website/ts/components/send_button.tsx +++ b/packages/website/ts/components/send_button.tsx @@ -80,7 +80,7 @@ export class SendButton extends React.Component<SendButtonProps, SendButtonState logUtils.log(`Unexpected error encountered: ${err}`); logUtils.log(err.stack); this.props.onError(); - await errorReporter.reportAsync(err); + errorReporter.report(err); } } this.setState({ diff --git a/packages/website/ts/components/token_balances.tsx b/packages/website/ts/components/token_balances.tsx index 3fae83c00..c8d80702b 100644 --- a/packages/website/ts/components/token_balances.tsx +++ b/packages/website/ts/components/token_balances.tsx @@ -5,7 +5,7 @@ import { Styles, utils as sharedUtils, } from '@0xproject/react-shared'; -import { BigNumber, errorUtils, logUtils } from '@0xproject/utils'; +import { BigNumber, errorUtils, fetchAsync, logUtils } from '@0xproject/utils'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; import * as _ from 'lodash'; import Dialog from 'material-ui/Dialog'; @@ -526,7 +526,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala this.setState({ errorType: BalanceErrs.mintingFailed, }); - await errorReporter.reportAsync(err); + errorReporter.report(err); return false; } } @@ -548,7 +548,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala await utils.sleepAsync(ARTIFICIAL_FAUCET_REQUEST_DELAY); const segment = isEtherRequest ? 'ether' : 'zrx'; - const response = await fetch( + const response = await fetchAsync( `${constants.URL_TESTNET_FAUCET}/${segment}/${this.props.userAddress}?networkId=${this.props.networkId}`, ); const responseBody = await response.text(); @@ -561,7 +561,7 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala this.setState({ errorType, }); - await errorReporter.reportAsync(new Error(`Faucet returned non-200: ${JSON.stringify(response)}`)); + errorReporter.report(new Error(`Faucet returned non-200: ${JSON.stringify(response)}`)); return false; } diff --git a/packages/website/ts/components/top_bar/top_bar.tsx b/packages/website/ts/components/top_bar/top_bar.tsx index 960e5a824..63ea3475a 100644 --- a/packages/website/ts/components/top_bar/top_bar.tsx +++ b/packages/website/ts/components/top_bar/top_bar.tsx @@ -7,7 +7,6 @@ import MenuItem from 'material-ui/MenuItem'; import * as React from 'react'; import { Link } from 'react-router-dom'; import { Blockchain } from 'ts/blockchain'; -import { LegacyPortalMenu } from 'ts/components/legacy_portal/legacy_portal_menu'; import { DrawerMenu } from 'ts/components/portal/drawer_menu'; import { ProviderDisplay } from 'ts/components/top_bar/provider_display'; import { TopBarMenuItem } from 'ts/components/top_bar/top_bar_menu_item'; @@ -17,7 +16,6 @@ import { Dispatcher } from 'ts/redux/dispatcher'; import { Deco, Key, ProviderType, WebsiteLegacyPaths, WebsitePaths } from 'ts/types'; import { constants } from 'ts/utils/constants'; import { Translate } from 'ts/utils/translate'; -import { utils } from 'ts/utils/utils'; export enum TopBarDisplayType { Default, @@ -213,8 +211,6 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> { </div> ); const popoverContent = <Menu style={{ color: colors.darkGrey }}>{developerSectionMenuItems}</Menu>; - // TODO : Remove this once we ship portal v2 - const shouldShowPortalV2Drawer = this._isViewingPortal() && utils.shouldShowPortalV2(); return ( <div style={{ ...styles.topBar, ...bottomBorderStyle, ...this.props.style, ...{ height } }} @@ -294,11 +290,11 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> { </div> </div> </Container> - {shouldShowPortalV2Drawer ? this._renderPortalV2Drawer() : this._renderDrawer()} + {this._isViewingPortal() ? this._renderPortalDrawer() : this._renderDrawer()} </div> ); } - private _renderPortalV2Drawer(): React.ReactNode { + private _renderPortalDrawer(): React.ReactNode { return ( <Drawer open={this.state.isDrawerOpen} @@ -326,7 +322,6 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> { onRequestChange={this._onMenuButtonClick.bind(this)} > <div className="clearfix"> - {this._renderPortalMenu()} {this._renderDocsMenu()} {this._renderWiki()} <div className="pl1 py1 mt3" style={{ backgroundColor: colors.lightGrey }}> @@ -478,20 +473,6 @@ export class TopBar extends React.Component<TopBarProps, TopBarState> { </div> ); } - private _renderPortalMenu(): React.ReactNode { - if (!this._isViewingPortal()) { - return undefined; - } - - return ( - <div className="lg-hide md-hide"> - <div className="pl1 py1" style={{ backgroundColor: colors.lightGrey }}> - {this.props.translate.get(Key.PortalDApp, Deco.CapWords)} - </div> - <LegacyPortalMenu menuItemStyle={{ color: 'black' }} onClick={this._onMenuButtonClick.bind(this)} /> - </div> - ); - } private _onMenuButtonClick(): void { this.setState({ isDrawerOpen: !this.state.isDrawerOpen, diff --git a/packages/website/ts/components/ui/menu_item.tsx b/packages/website/ts/components/ui/menu_item.tsx index 64c0dc49d..0cb4b387c 100644 --- a/packages/website/ts/components/ui/menu_item.tsx +++ b/packages/website/ts/components/ui/menu_item.tsx @@ -15,7 +15,7 @@ interface MenuItemState { export class MenuItem extends React.Component<MenuItemProps, MenuItemState> { public static defaultProps: Partial<MenuItemProps> = { - onClick: _.noop, + onClick: _.noop.bind(_), className: '', }; public constructor(props: MenuItemProps) { diff --git a/packages/website/ts/components/ui/overlay.tsx b/packages/website/ts/components/ui/overlay.tsx index da26317de..fc7507475 100644 --- a/packages/website/ts/components/ui/overlay.tsx +++ b/packages/website/ts/components/ui/overlay.tsx @@ -26,7 +26,7 @@ export const Overlay: React.StatelessComponent<OverlayProps> = props => ( Overlay.defaultProps = { style: {}, - onClick: _.noop, + onClick: _.noop.bind(_), }; Overlay.displayName = 'Overlay'; diff --git a/packages/website/ts/components/ui/simple_menu.tsx b/packages/website/ts/components/ui/simple_menu.tsx index 74b8ef6ae..8a9349c6d 100644 --- a/packages/website/ts/components/ui/simple_menu.tsx +++ b/packages/website/ts/components/ui/simple_menu.tsx @@ -41,7 +41,7 @@ export const SimpleMenuItem: React.StatelessComponent<SimpleMenuItemProps> = ({ <Text fontSize="14px" fontColor={colors.darkGrey} - onClick={onClick || _.noop} + onClick={onClick || _.noop.bind(_)} hoverColor={colors.mediumBlue} > {displayText} @@ -84,5 +84,5 @@ export interface DifferentWalletSimpleMenuItemProps { export const DifferentWalletSimpleMenuItem: React.StatelessComponent<DifferentWalletSimpleMenuItemProps> = ({ onClick, }) => { - return <SimpleMenuItem displayText="Use a Different Wallet..." onClick={onClick} />; + return <SimpleMenuItem displayText="Use Ledger Wallet..." onClick={onClick} />; }; diff --git a/packages/website/ts/components/wallet/wallet.tsx b/packages/website/ts/components/wallet/wallet.tsx index 6c1c495d7..40a8a23ea 100644 --- a/packages/website/ts/components/wallet/wallet.tsx +++ b/packages/website/ts/components/wallet/wallet.tsx @@ -1,4 +1,4 @@ -import { constants as sharedConstants, EtherscanLinkSuffixes, utils as sharedUtils } from '@0xproject/react-shared'; +import { EtherscanLinkSuffixes, utils as sharedUtils } from '@0xproject/react-shared'; import { BigNumber, errorUtils } from '@0xproject/utils'; import * as _ from 'lodash'; @@ -204,7 +204,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> { <AccountConnection accountState={accountState} injectedProviderName={this.props.injectedProviderName} /> </div> ); - const onClick = _.noop; + const onClick = _.noop.bind(_); const accessory = ( <DropDown activeNode={ @@ -488,19 +488,17 @@ export class Wallet extends React.Component<WalletProps, WalletState> { ); } 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); + analytics.track(action); this.setState({ wrappedEtherDirection, }); } 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); + analytics.track(action); 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 2b4cf93fe..fcab5d1dd 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 { constants as sharedConstants, Styles } from '@0xproject/react-shared'; +import { Styles } from '@0xproject/react-shared'; import { BigNumber, logUtils } from '@0xproject/utils'; import { Web3Wrapper } from '@0xproject/web3-wrapper'; import * as _ from 'lodash'; @@ -188,20 +188,23 @@ export class WrapEtherItem extends React.Component<WrapEtherItemProps, WrapEther this.setState({ isEthConversionHappening: true, }); - const networkName = sharedConstants.NETWORK_NAME_BY_ID[this.props.networkId]; + const etherToken = this.props.etherToken; + const amountToConvert = this.state.currentInputAmount; + const ethAmount = Web3Wrapper.toUnitAmount(amountToConvert, constants.DECIMAL_PLACES_ETH).toString(); + const tokenAmount = Web3Wrapper.toUnitAmount(amountToConvert, etherToken.decimals).toString(); try { - const etherToken = this.props.etherToken; - const amountToConvert = this.state.currentInputAmount; if (this.props.direction === Side.Deposit) { 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); + this.props.dispatcher.showFlashMessage(`Successfully wrapped ${ethAmount} ETH to WETH`); + analytics.track('Wrap ETH Success', { + amount: ethAmount, + }); } 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); + this.props.dispatcher.showFlashMessage(`Successfully unwrapped ${tokenAmount} WETH to ETH`); + analytics.track('Unwrap WETH Success', { + amount: tokenAmount, + }); } await this.props.refetchEthTokenStateAsync(); this.props.onConversionSuccessful(); @@ -214,12 +217,16 @@ export class WrapEtherItem extends React.Component<WrapEtherItemProps, WrapEther logUtils.log(err.stack); 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); + analytics.track('Wrap ETH Failure', { + amount: ethAmount, + }); } else { this.props.dispatcher.showFlashMessage('Failed to unwrap your WETH. Please try again.'); - analytics.logEvent('Portal', 'Unwrap WETH Failed', networkName); + analytics.track('Unwrap WETH Failed', { + amount: tokenAmount, + }); } - await errorReporter.reportAsync(err); + errorReporter.report(err); } } this.setState({ diff --git a/packages/website/ts/containers/legacy_portal.ts b/packages/website/ts/containers/legacy_portal.ts deleted file mode 100644 index e99f47fb7..000000000 --- a/packages/website/ts/containers/legacy_portal.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { BigNumber } from '@0xproject/utils'; -import * as _ from 'lodash'; -import * as React from 'react'; -import { connect } from 'react-redux'; -import { Dispatch } from 'redux'; -import { - LegacyPortal as LegacyPortalComponent, - LegacyPortalProps as LegacyPortalComponentProps, -} from 'ts/components/legacy_portal/legacy_portal'; -import { Dispatcher } from 'ts/redux/dispatcher'; -import { State } from 'ts/redux/reducer'; -import { BlockchainErrs, HashData, Order, ProviderType, ScreenWidths, Side, TokenByAddress } from 'ts/types'; -import { constants } from 'ts/utils/constants'; -import { Translate } from 'ts/utils/translate'; - -interface ConnectedState { - blockchainErr: BlockchainErrs; - blockchainIsLoaded: boolean; - hashData: HashData; - injectedProviderName: string; - networkId: number; - nodeVersion: string; - orderFillAmount: BigNumber; - providerType: ProviderType; - tokenByAddress: TokenByAddress; - lastForceTokenStateRefetch: number; - userEtherBalanceInWei?: BigNumber; - screenWidth: ScreenWidths; - shouldBlockchainErrDialogBeOpen: boolean; - userAddress: string; - userSuppliedOrderCache: Order; - flashMessage?: string | React.ReactNode; - translate: Translate; -} - -interface ConnectedDispatch { - dispatcher: Dispatcher; -} - -const mapStateToProps = (state: State, _ownProps: LegacyPortalComponentProps): ConnectedState => { - const receiveAssetToken = state.sideToAssetToken[Side.Receive]; - const depositAssetToken = state.sideToAssetToken[Side.Deposit]; - const receiveAddress = !_.isUndefined(receiveAssetToken.address) - ? receiveAssetToken.address - : constants.NULL_ADDRESS; - const depositAddress = !_.isUndefined(depositAssetToken.address) - ? depositAssetToken.address - : constants.NULL_ADDRESS; - const receiveAmount = !_.isUndefined(receiveAssetToken.amount) ? receiveAssetToken.amount : new BigNumber(0); - const depositAmount = !_.isUndefined(depositAssetToken.amount) ? depositAssetToken.amount : new BigNumber(0); - const hashData = { - depositAmount, - depositTokenContractAddr: depositAddress, - feeRecipientAddress: constants.NULL_ADDRESS, - makerFee: constants.MAKER_FEE, - orderExpiryTimestamp: state.orderExpiryTimestamp, - orderMakerAddress: state.userAddress, - orderTakerAddress: state.orderTakerAddress !== '' ? state.orderTakerAddress : constants.NULL_ADDRESS, - receiveAmount, - receiveTokenContractAddr: receiveAddress, - takerFee: constants.TAKER_FEE, - orderSalt: state.orderSalt, - }; - return { - blockchainErr: state.blockchainErr, - blockchainIsLoaded: state.blockchainIsLoaded, - hashData, - injectedProviderName: state.injectedProviderName, - networkId: state.networkId, - nodeVersion: state.nodeVersion, - orderFillAmount: state.orderFillAmount, - providerType: state.providerType, - screenWidth: state.screenWidth, - shouldBlockchainErrDialogBeOpen: state.shouldBlockchainErrDialogBeOpen, - tokenByAddress: state.tokenByAddress, - lastForceTokenStateRefetch: state.lastForceTokenStateRefetch, - userAddress: state.userAddress, - userEtherBalanceInWei: state.userEtherBalanceInWei, - userSuppliedOrderCache: state.userSuppliedOrderCache, - flashMessage: state.flashMessage, - translate: state.translate, - }; -}; - -const mapDispatchToProps = (dispatch: Dispatch<State>): ConnectedDispatch => ({ - dispatcher: new Dispatcher(dispatch), -}); - -export const LegacyPortal: React.ComponentClass<LegacyPortalComponentProps> = connect( - mapStateToProps, - mapDispatchToProps, -)(LegacyPortalComponent); diff --git a/packages/website/ts/containers/zero_ex_js_documentation.ts b/packages/website/ts/containers/zero_ex_js_documentation.ts index b43a1f645..2fc08e923 100644 --- a/packages/website/ts/containers/zero_ex_js_documentation.ts +++ b/packages/website/ts/containers/zero_ex_js_documentation.ts @@ -183,6 +183,32 @@ const docsInfoConfig: DocsInfoConfig = { 'JSONRPCErrorCallback', 'LogEntryEvent', 'LogEntry', + 'ERC20AssetData', + 'ERC721AssetData', + 'AssetProxyId', + 'WETH9Events', + 'WETH9WithdrawalEventArgs', + 'WETH9ApprovalEventArgs', + 'WETH9EventArgs', + 'WETH9DepositEventArgs', + 'WETH9TransferEventArgs', + 'ERC20TokenTransferEventArgs', + 'ERC20TokenApprovalEventArgs', + 'ERC20TokenEvents', + 'ERC20TokenEventArgs', + 'ERC721TokenApprovalEventArgs', + 'ERC721TokenApprovalForAllEventArgs', + 'ERC721TokenTransferEventArgs', + 'ERC721TokenEvents', + 'ExchangeCancelUpToEventArgs', + 'ExchangeAssetProxyRegisteredEventArgs', + 'ExchangeFillEventArgs', + 'ExchangeCancelEventArgs', + 'ExchangeEventArgs', + 'ContractWrappersConfig', + 'MessagePrefixType', + 'MessagePrefixOpts', + 'OrderInfo', ], typeNameToPrefix: {}, typeNameToExternalLink: { diff --git a/packages/website/ts/index.tsx b/packages/website/ts/index.tsx index c7ccfdf1f..2a5c5e4f1 100644 --- a/packages/website/ts/index.tsx +++ b/packages/website/ts/index.tsx @@ -16,11 +16,9 @@ import { trackedTokenStorage } from 'ts/local_storage/tracked_token_storage'; import { tradeHistoryStorage } from 'ts/local_storage/trade_history_storage'; import { store } from 'ts/redux/store'; import { WebsiteLegacyPaths, WebsitePaths } from 'ts/types'; -import { analytics } from 'ts/utils/analytics'; import { muiTheme } from 'ts/utils/mui_theme'; import { utils } from 'ts/utils/utils'; // Polyfills -import 'whatwg-fetch'; injectTapEventPlugin(); // Check if we've introduced an update that requires us to clear the tradeHistory local storage entries @@ -35,14 +33,9 @@ import 'less/all.less'; // At the same time webpack statically parses for System.import() to determine bundle chunk split points // so each lazy import needs it's own `System.import()` declaration. -// TODO: Remove this once we ship V2 -const LazyPortal = utils.shouldShowPortalV2() - ? createLazyComponent('Portal', async () => - System.import<any>(/* webpackChunkName: "portal" */ 'ts/containers/portal'), - ) - : createLazyComponent('LegacyPortal', async () => - System.import<any>(/* webpackChunkName: "legacyPortal" */ 'ts/containers/legacy_portal'), - ); +const LazyPortal = createLazyComponent('Portal', async () => + System.import<any>(/* webpackChunkName: "portal" */ 'ts/containers/portal'), +); const LazyZeroExJSDocumentation = createLazyComponent('Documentation', async () => System.import<any>(/* webpackChunkName: "zeroExDocs" */ 'ts/containers/zero_ex_js_documentation'), ); @@ -74,10 +67,6 @@ const LazyEthereumTypesDocumentation = createLazyComponent('Documentation', asyn System.import<any>(/* webpackChunkName: "ethereumTypesDocs" */ 'ts/containers/ethereum_types_documentation'), ); -analytics.init(); -// tslint:disable-next-line:no-floating-promises -analytics.logProviderAsync((window as any).web3); - render( <Router> <div> diff --git a/packages/website/ts/local_storage/local_storage.ts b/packages/website/ts/local_storage/local_storage.ts index 1e3258ce0..13d9ca401 100644 --- a/packages/website/ts/local_storage/local_storage.ts +++ b/packages/website/ts/local_storage/local_storage.ts @@ -5,7 +5,7 @@ export const localStorage = { return !!window.localStorage; }, getItemIfExists(key: string): string { - if (!this.doesExist) { + if (!localStorage.doesExist) { return undefined; } const item = window.localStorage.getItem(key); @@ -15,13 +15,13 @@ export const localStorage = { return item; }, setItem(key: string, value: string): void { - if (!this.doesExist || _.isUndefined(value)) { + if (!localStorage.doesExist || _.isUndefined(value)) { return; } window.localStorage.setItem(key, value); }, removeItem(key: string): void { - if (!this.doesExist) { + if (!localStorage.doesExist) { return; } window.localStorage.removeItem(key); @@ -37,7 +37,7 @@ export const localStorage = { localStorage.setItem(key, JSON.stringify(value)); }, getAllKeys(): string[] { - if (!this.doesExist) { + if (!localStorage.doesExist) { return []; } return _.keys(window.localStorage); diff --git a/packages/website/ts/local_storage/tracked_token_storage.ts b/packages/website/ts/local_storage/tracked_token_storage.ts index f865f8109..b1b579aef 100644 --- a/packages/website/ts/local_storage/tracked_token_storage.ts +++ b/packages/website/ts/local_storage/tracked_token_storage.ts @@ -17,7 +17,7 @@ export const trackedTokenStorage = { localStorage.setItem(TRACKED_TOKENS_CLEAR_KEY, configs.LAST_LOCAL_STORAGE_TRACKED_TOKEN_CLEARANCE_DATE); }, addTrackedTokenToUser(userAddress: string, networkId: number, token: Token): void { - const trackedTokensByUserAddress = this.getTrackedTokensByUserAddress(); + const trackedTokensByUserAddress = trackedTokenStorage.getTrackedTokensByUserAddress(); let trackedTokensByNetworkId = trackedTokensByUserAddress[userAddress]; if (_.isUndefined(trackedTokensByNetworkId)) { trackedTokensByNetworkId = {}; @@ -57,7 +57,7 @@ export const trackedTokenStorage = { return trackedTokensByAddress; }, removeTrackedToken(userAddress: string, networkId: number, tokenAddress: string): void { - const trackedTokensByUserAddress = this.getTrackedTokensByUserAddress(); + const trackedTokensByUserAddress = trackedTokenStorage.getTrackedTokensByUserAddress(); const trackedTokensByNetworkId = trackedTokensByUserAddress[userAddress]; const trackedTokens = trackedTokensByNetworkId[networkId]; const remainingTrackedTokens = _.filter(trackedTokens, (token: Token) => { diff --git a/packages/website/ts/local_storage/trade_history_storage.tsx b/packages/website/ts/local_storage/trade_history_storage.tsx index 2e2f4e64e..a9b042820 100644 --- a/packages/website/ts/local_storage/trade_history_storage.tsx +++ b/packages/website/ts/local_storage/trade_history_storage.tsx @@ -27,31 +27,31 @@ export const tradeHistoryStorage = { localStorage.setItem(FILL_CLEAR_KEY, configs.LAST_LOCAL_STORAGE_FILL_CLEARANCE_DATE); }, addFillToUser(userAddress: string, networkId: number, fill: Fill): void { - const fillsByHash = this.getUserFillsByHash(userAddress, networkId); - const fillHash = this._getFillHash(fill); + const fillsByHash = tradeHistoryStorage.getUserFillsByHash(userAddress, networkId); + const fillHash = tradeHistoryStorage._getFillHash(fill); const doesFillExist = !_.isUndefined(fillsByHash[fillHash]); if (doesFillExist) { return; // noop } fillsByHash[fillHash] = fill; const userFillsJSONString = JSON.stringify(fillsByHash); - const userFillsKey = this._getUserFillsKey(userAddress, networkId); + const userFillsKey = tradeHistoryStorage._getUserFillsKey(userAddress, networkId); localStorage.setItem(userFillsKey, userFillsJSONString); }, removeFillFromUser(userAddress: string, networkId: number, fill: Fill): void { - const fillsByHash = this.getUserFillsByHash(userAddress, networkId); - const fillHash = this._getFillHash(fill); + const fillsByHash = tradeHistoryStorage.getUserFillsByHash(userAddress, networkId); + const fillHash = tradeHistoryStorage._getFillHash(fill); const doesFillExist = !_.isUndefined(fillsByHash[fillHash]); if (!doesFillExist) { return; // noop } delete fillsByHash[fillHash]; const userFillsJSONString = JSON.stringify(fillsByHash); - const userFillsKey = this._getUserFillsKey(userAddress, networkId); + const userFillsKey = tradeHistoryStorage._getUserFillsKey(userAddress, networkId); localStorage.setItem(userFillsKey, userFillsJSONString); }, getUserFillsByHash(userAddress: string, networkId: number): { [fillHash: string]: Fill } { - const userFillsKey = this._getUserFillsKey(userAddress, networkId); + const userFillsKey = tradeHistoryStorage._getUserFillsKey(userAddress, networkId); const userFillsJSONString = localStorage.getItemIfExists(userFillsKey); if (_.isEmpty(userFillsJSONString)) { return {}; @@ -66,7 +66,7 @@ export const tradeHistoryStorage = { return userFillsByHash; }, getFillsLatestBlock(userAddress: string, networkId: number): number { - const userFillsLatestBlockKey = this._getFillsLatestBlockKey(userAddress, networkId); + const userFillsLatestBlockKey = tradeHistoryStorage._getFillsLatestBlockKey(userAddress, networkId); const blockNumberStr = localStorage.getItemIfExists(userFillsLatestBlockKey); if (_.isEmpty(blockNumberStr)) { return constants.GENESIS_ORDER_BLOCK_BY_NETWORK_ID[networkId]; @@ -75,7 +75,7 @@ export const tradeHistoryStorage = { return blockNumber; }, setFillsLatestBlock(userAddress: string, networkId: number, blockNumber: number): void { - const userFillsLatestBlockKey = this._getFillsLatestBlockKey(userAddress, networkId); + const userFillsLatestBlockKey = tradeHistoryStorage._getFillsLatestBlockKey(userAddress, networkId); localStorage.setItem(userFillsLatestBlockKey, `${blockNumber}`); }, _getUserFillsKey(userAddress: string, networkId: number): string { diff --git a/packages/website/ts/pages/about/about.tsx b/packages/website/ts/pages/about/about.tsx index 5bb5d06a9..33581c938 100644 --- a/packages/website/ts/pages/about/about.tsx +++ b/packages/website/ts/pages/about/about.tsx @@ -99,7 +99,7 @@ const teamRow3: ProfileInfo[] = [ }, { name: 'Jacob Evans', - title: 'Blockchain Engineer', + title: 'Ecosystem Engineer', description: `Previously software engineer at Qantas and RSA Security.`, image: '/images/team/jacob.jpg', linkedIn: 'https://www.linkedin.com/in/dekzter/', @@ -174,14 +174,21 @@ const teamRow6: ProfileInfo[] = [ linkedIn: 'https://www.linkedin.com/in/stephenalexbrowne/', github: 'http://github.com/albrow', }, - // { - // name: 'Chris Kalani', - // title: 'Director of Design', - // description: `Previously founded Wake (acquired by InVision). Early Facebook product designer.`, - // image: 'images/team/chris.png', - // linkedIn: 'https://www.linkedin.com/in/chriskalani/', - // github: 'https://github.com/chriskalani', - // }, + { + name: 'Peter Zeitz', + title: 'Research Fellow', + description: `Researching decentralized governance. Previously Assistant Professor of Economics at National University of Singapore Business School. PhD in Economics at UCLA.`, + image: 'images/team/peter.jpg', + linkedIn: 'https://www.linkedin.com/in/peter-z-7b9595163/', + }, + { + name: 'Chris Kalani', + title: 'Director of Design', + description: `Previously founded Wake (acquired by InVision). Early Facebook product designer.`, + image: 'images/team/chris.png', + linkedIn: 'https://www.linkedin.com/in/chriskalani/', + github: 'https://github.com/chriskalani', + }, ]; const advisors: ProfileInfo[] = [ diff --git a/packages/website/ts/pages/jobs/jobs.tsx b/packages/website/ts/pages/jobs/jobs.tsx index 314669ee9..38cefa832 100644 --- a/packages/website/ts/pages/jobs/jobs.tsx +++ b/packages/website/ts/pages/jobs/jobs.tsx @@ -33,7 +33,7 @@ export interface JobsState {} export class Jobs extends React.Component<JobsProps, JobsState> { // TODO: consolidate this small screen scaffolding into one place (its being used in portal and docs as well) - private _throttledScreenWidthUpdate: () => void; + private readonly _throttledScreenWidthUpdate: () => void; public constructor(props: JobsProps) { super(props); this._throttledScreenWidthUpdate = _.throttle(this._updateScreenWidth.bind(this), THROTTLE_TIMEOUT); diff --git a/packages/website/ts/pages/landing/landing.tsx b/packages/website/ts/pages/landing/landing.tsx index b2cf4d979..2a51ee3c0 100644 --- a/packages/website/ts/pages/landing/landing.tsx +++ b/packages/website/ts/pages/landing/landing.tsx @@ -171,7 +171,7 @@ interface LandingState { } export class Landing extends React.Component<LandingProps, LandingState> { - private _throttledScreenWidthUpdate: () => void; + private readonly _throttledScreenWidthUpdate: () => void; constructor(props: LandingProps) { super(props); this.state = { diff --git a/packages/website/ts/pages/wiki/wiki.tsx b/packages/website/ts/pages/wiki/wiki.tsx index 9659900be..55f532b11 100644 --- a/packages/website/ts/pages/wiki/wiki.tsx +++ b/packages/website/ts/pages/wiki/wiki.tsx @@ -205,7 +205,7 @@ export class Wiki extends React.Component<WikiProps, WikiState> { articlesBySection, }, async () => { - await utils.onPageLoadAsync(); + await utils.onPageLoadPromise; const hash = this.props.location.hash.slice(1); sharedUtils.scrollToHash(hash, sharedConstants.SCROLL_CONTAINER_ID); }, diff --git a/packages/website/ts/redux/analyticsMiddleware.ts b/packages/website/ts/redux/analyticsMiddleware.ts new file mode 100644 index 000000000..51d39a5d7 --- /dev/null +++ b/packages/website/ts/redux/analyticsMiddleware.ts @@ -0,0 +1,36 @@ +import { Middleware } from 'redux'; +import { State } from 'ts/redux/reducer'; +import { ActionTypes } from 'ts/types'; +import { analytics } from 'ts/utils/analytics'; + +export const analyticsMiddleware: Middleware = store => next => action => { + const nextAction = next(action); + const nextState = (store.getState() as any) as State; + switch (action.type) { + case ActionTypes.UpdateInjectedProviderName: + analytics.addEventProperties({ + injectedProviderName: nextState.injectedProviderName, + }); + break; + case ActionTypes.UpdateNetworkId: + analytics.addEventProperties({ + networkId: nextState.networkId, + }); + break; + case ActionTypes.UpdateUserAddress: + analytics.addUserProperties({ + ethAddress: nextState.userAddress, + }); + break; + case ActionTypes.UpdateUserEtherBalance: + if (nextState.userEtherBalanceInWei) { + analytics.addUserProperties({ + ethBalance: nextState.userEtherBalanceInWei.toString(), + }); + } + break; + default: + break; + } + return nextAction; +}; diff --git a/packages/website/ts/redux/dispatcher.ts b/packages/website/ts/redux/dispatcher.ts index e0ce43ae5..db008d319 100644 --- a/packages/website/ts/redux/dispatcher.ts +++ b/packages/website/ts/redux/dispatcher.ts @@ -17,7 +17,7 @@ import { } from 'ts/types'; export class Dispatcher { - private _dispatch: Dispatch<State>; + private readonly _dispatch: Dispatch<State>; constructor(dispatch: Dispatch<State>) { this._dispatch = dispatch; } diff --git a/packages/website/ts/redux/store.ts b/packages/website/ts/redux/store.ts index 2672e3f61..006241371 100644 --- a/packages/website/ts/redux/store.ts +++ b/packages/website/ts/redux/store.ts @@ -1,7 +1,8 @@ import * as _ from 'lodash'; -import { createStore, Store as ReduxStore } from 'redux'; -import { devToolsEnhancer } from 'redux-devtools-extension/developmentOnly'; +import { applyMiddleware, createStore, Store as ReduxStore } from 'redux'; +import { composeWithDevTools } from 'redux-devtools-extension/developmentOnly'; import { stateStorage } from 'ts/local_storage/state_storage'; +import { analyticsMiddleware } from 'ts/redux/analyticsMiddleware'; import { reducer, State } from 'ts/redux/reducer'; const ONE_SECOND = 1000; @@ -9,7 +10,7 @@ const ONE_SECOND = 1000; export const store: ReduxStore<State> = createStore( reducer, stateStorage.getPersistedDefaultState(), - devToolsEnhancer({ name: '0x Website Redux Store' }), + composeWithDevTools(applyMiddleware(analyticsMiddleware)), ); store.subscribe( _.throttle(() => { diff --git a/packages/website/ts/types.ts b/packages/website/ts/types.ts index e8dc694f6..4d0496f6c 100644 --- a/packages/website/ts/types.ts +++ b/packages/website/ts/types.ts @@ -244,7 +244,10 @@ export enum BlockchainCallErrs { export enum Environments { DEVELOPMENT = 'DEVELOPMENT', + DOGFOOD = 'DOGFOOD', + STAGING = 'STAGING', PRODUCTION = 'PRODUCTION', + UNKNOWN = 'UNKNOWN', } export type ContractInstance = any; // TODO: add type definition for Contract @@ -516,8 +519,10 @@ export interface OutdatedWrappedEtherByNetworkId { }; } -export interface ItemByAddress<T> { - [address: string]: T; +export type ItemByAddress<T> = ObjectMap<T>; + +export interface ObjectMap<T> { + [key: string]: T; } export type TokenStateByAddress = ItemByAddress<TokenState>; diff --git a/packages/website/ts/utils/analytics.ts b/packages/website/ts/utils/analytics.ts index f4bfa083f..e5a1ddfa4 100644 --- a/packages/website/ts/utils/analytics.ts +++ b/packages/website/ts/utils/analytics.ts @@ -1,27 +1,83 @@ import * as _ from 'lodash'; -import * as ReactGA from 'react-ga'; -import { InjectedWeb3 } from 'ts/types'; -import { configs } from 'ts/utils/configs'; +import { ObjectMap, Order } from 'ts/types'; import { utils } from 'ts/utils/utils'; -export const analytics = { - init(): void { - ReactGA.initialize(configs.GOOGLE_ANALYTICS_ID); - }, - logEvent(category: string, action: string, label: string, value?: any): void { - ReactGA.event({ - category, - action, - label, - value, - }); - }, - async logProviderAsync(web3IfExists: InjectedWeb3): Promise<void> { - await utils.onPageLoadAsync(); - const providerType = - !_.isUndefined(web3IfExists) && !_.isUndefined(web3IfExists.currentProvider) - ? utils.getProviderType(web3IfExists.currentProvider) - : 'NONE'; - ReactGA.ga('set', 'dimension1', providerType); - }, -}; +export interface HeapAnalytics { + loaded: boolean; + identify(id: string, idType: string): void; + track(eventName: string, eventProperties?: ObjectMap<string | number>): void; + resetIdentity(): void; + addUserProperties(properties: ObjectMap<string | number>): void; + addEventProperties(properties: ObjectMap<string | number>): void; + removeEventProperty(property: string): void; + clearEventProperties(): void; +} +export class Analytics { + private _heap: HeapAnalytics; + public static init(): Analytics { + return new Analytics(Analytics.getHeap()); + } + public static getHeap(): HeapAnalytics { + const heap = (window as any).heap; + if (!_.isUndefined(heap)) { + return heap; + } else { + throw new Error('Could not find the Heap SDK on the page.'); + } + } + constructor(heap: HeapAnalytics) { + this._heap = heap; + } + // tslint:disable:no-floating-promises + // HeapAnalytics Wrappers + public identify(id: string, idType: string): void { + this._heapLoadedGuardAsync(() => this._heap.identify(id, idType)); + } + public track(eventName: string, eventProperties?: ObjectMap<string | number>): void { + this._heapLoadedGuardAsync(() => this._heap.track(eventName, eventProperties)); + } + public resetIdentity(): void { + this._heapLoadedGuardAsync(() => this._heap.resetIdentity()); + } + public addUserProperties(properties: ObjectMap<string | number>): void { + this._heapLoadedGuardAsync(() => this._heap.addUserProperties(properties)); + } + public addEventProperties(properties: ObjectMap<string | number>): void { + this._heapLoadedGuardAsync(() => this._heap.addEventProperties(properties)); + } + public removeEventProperty(property: string): void { + this._heapLoadedGuardAsync(() => this._heap.removeEventProperty(property)); + } + public clearEventProperties(): void { + this._heapLoadedGuardAsync(() => this._heap.clearEventProperties()); + } + // tslint:enable:no-floating-promises + // Custom methods + public trackOrderEvent(eventName: string, order: Order): void { + const orderLoggingData = { + takerTokenAmount: order.signedOrder.takerTokenAmount, + makeTokenAmount: order.signedOrder.makerTokenAmount, + takerToken: order.metadata.takerToken.symbol, + makerToken: order.metadata.makerToken.symbol, + }; + this.track(eventName, orderLoggingData); + } + /** + * Heap is not available as a UMD module, and additionally has the strange property of replacing itself with + * a different object once it's loaded. + * Instead of having an await call before every analytics use, we opt to have the awaiting logic in here by + * guarding every API call with the guard below. + */ + private async _heapLoadedGuardAsync(callback: () => void): Promise<void> { + if (this._heap.loaded) { + callback(); + return undefined; + } + await utils.onPageLoadPromise; + // HACK: Reset heap to loaded heap + this._heap = (window as any).heap; + callback(); + } +} + +export const analytics = Analytics.init(); diff --git a/packages/website/ts/utils/backend_client.ts b/packages/website/ts/utils/backend_client.ts index 835a6ef4d..5164211df 100644 --- a/packages/website/ts/utils/backend_client.ts +++ b/packages/website/ts/utils/backend_client.ts @@ -6,6 +6,7 @@ import { WebsiteBackendJobInfo, WebsiteBackendPriceInfo, WebsiteBackendRelayerInfo, + WebsiteBackendTokenInfo, } from 'ts/types'; import { fetchUtils } from 'ts/utils/fetch_utils'; import { utils } from 'ts/utils/utils'; @@ -14,6 +15,7 @@ const ETH_GAS_STATION_ENDPOINT = '/eth_gas_station'; const JOBS_ENDPOINT = '/jobs'; const PRICES_ENDPOINT = '/prices'; const RELAYERS_ENDPOINT = '/relayers'; +const TOKENS_ENDPOINT = '/tokens'; const WIKI_ENDPOINT = '/wiki'; const SUBSCRIBE_SUBSTACK_NEWSLETTER_ENDPOINT = '/newsletter_subscriber/substack'; @@ -41,6 +43,10 @@ export const backendClient = { const result = await fetchUtils.requestAsync(utils.getBackendBaseUrl(), RELAYERS_ENDPOINT); return result; }, + async getTokenInfosAsync(): Promise<WebsiteBackendTokenInfo[]> { + const result = await fetchUtils.requestAsync(utils.getBackendBaseUrl(), TOKENS_ENDPOINT); + return result; + }, async getWikiArticlesBySectionAsync(): Promise<ArticlesBySection> { const result = await fetchUtils.requestAsync(utils.getBackendBaseUrl(), WIKI_ENDPOINT); return result; diff --git a/packages/website/ts/utils/configs.ts b/packages/website/ts/utils/configs.ts index 97aabd13d..a1c64f9cb 100644 --- a/packages/website/ts/utils/configs.ts +++ b/packages/website/ts/utils/configs.ts @@ -1,11 +1,6 @@ -import * as _ from 'lodash'; -import { Environments, OutdatedWrappedEtherByNetworkId, PublicNodeUrlsByNetworkId } from 'ts/types'; +import { OutdatedWrappedEtherByNetworkId, PublicNodeUrlsByNetworkId } from 'ts/types'; const BASE_URL = window.location.origin; -const isDevelopment = _.includes( - ['https://0xproject.localhost:3572', 'https://localhost:3572', 'https://127.0.0.1'], - BASE_URL, -); const INFURA_API_KEY = 'T5WSC8cautR4KXyYgsRs'; export const configs = { @@ -19,9 +14,8 @@ export const configs = { DEFAULT_TRACKED_TOKEN_SYMBOLS: ['WETH', 'ZRX'], DOMAIN_STAGING: 'staging-0xproject.s3-website-us-east-1.amazonaws.com', DOMAIN_DOGFOOD: 'dogfood.0xproject.com', - DOMAIN_DEVELOPMENT: '0xproject.localhost:3572', + DOMAINS_DEVELOPMENT: ['0xproject.localhost:3572', 'localhost:3572', '127.0.0.1'], DOMAIN_PRODUCTION: '0xproject.com', - ENVIRONMENT: isDevelopment ? Environments.DEVELOPMENT : Environments.PRODUCTION, GOOGLE_ANALYTICS_ID: 'UA-98720122-1', LAST_LOCAL_STORAGE_FILL_CLEARANCE_DATE: '2017-11-22', LAST_LOCAL_STORAGE_TRACKED_TOKEN_CLEARANCE_DATE: '2018-7-5', diff --git a/packages/website/ts/utils/constants.ts b/packages/website/ts/utils/constants.ts index 4b3443d21..20441cd75 100644 --- a/packages/website/ts/utils/constants.ts +++ b/packages/website/ts/utils/constants.ts @@ -33,7 +33,7 @@ export const constants = { PROVIDER_NAME_TOSHI: 'Toshi', PROVIDER_NAME_GENERIC: 'Injected Web3', PROVIDER_NAME_PUBLIC: '0x Public', - ROLLBAR_ACCESS_TOKEN: 'a6619002b51c4464928201e6ea94de65', + ROLLBAR_ACCESS_TOKEN: '32c39bfa4bb6440faedc1612a9c13d28', S3_DOC_BUCKET_ROOT: 'https://s3.amazonaws.com/doc-jsons', S3_STAGING_DOC_BUCKET_ROOT: 'https://s3.amazonaws.com/staging-doc-jsons', SUCCESS_STATUS: 200, @@ -58,7 +58,7 @@ export const constants = { PROJECT_URL_MAKER: 'https://makerdao.com', PROJECT_URL_ARAGON: 'https://aragon.one', PROJECT_URL_BLOCKNET: 'https://blocknet.co', - PROJECT_URL_0CEAN: 'http://the0cean.com', + PROJECT_URL_0CEAN: 'https://theocean.trade', PROJECT_URL_IMTOKEN: 'https://tokenlon.token.im/', PROJECT_URL_AUGUR: 'https://augur.net', PROJECT_URL_AUCTUS: 'https://auctus.org', diff --git a/packages/website/ts/utils/doc_utils.ts b/packages/website/ts/utils/doc_utils.ts index 7768835fb..1627b9b0c 100644 --- a/packages/website/ts/utils/doc_utils.ts +++ b/packages/website/ts/utils/doc_utils.ts @@ -1,5 +1,5 @@ import { DoxityDocObj, TypeDocNode } from '@0xproject/react-docs'; -import { logUtils } from '@0xproject/utils'; +import { fetchAsync, logUtils } from '@0xproject/utils'; import findVersions = require('find-versions'); import * as _ from 'lodash'; import { S3FileObject, VersionToFilePath } from 'ts/types'; @@ -16,7 +16,7 @@ export const docUtils = { return versionToFilePath; }, async getVersionFileNamesAsync(s3DocJsonRoot: string, folderName: string): Promise<string[]> { - const response = await fetch(s3DocJsonRoot); + const response = await fetchAsync(s3DocJsonRoot); if (response.status !== 200) { // TODO: Show the user an error message when the docs fail to load const errMsg = await response.text(); @@ -73,7 +73,7 @@ export const docUtils = { }, async getJSONDocFileAsync(filePath: string, s3DocJsonRoot: string): Promise<TypeDocNode | DoxityDocObj> { const endpoint = `${s3DocJsonRoot}/${filePath}`; - const response = await fetch(endpoint); + const response = await fetchAsync(endpoint); if (response.status !== 200) { // TODO: Show the user an error message when the docs fail to load const errMsg = await response.text(); diff --git a/packages/website/ts/utils/error_reporter.ts b/packages/website/ts/utils/error_reporter.ts index f875141fe..6008fffed 100644 --- a/packages/website/ts/utils/error_reporter.ts +++ b/packages/website/ts/utils/error_reporter.ts @@ -1,7 +1,7 @@ import { logUtils } from '@0xproject/utils'; -import { Environments } from 'ts/types'; import { configs } from 'ts/utils/configs'; import { constants } from 'ts/utils/constants'; +import { utils } from 'ts/utils/utils'; // Suggested way to include Rollbar with Webpack // https://github.com/rollbar/rollbar.js/tree/master/examples/webpack @@ -12,7 +12,15 @@ const rollbarConfig = { itemsPerMinute: 10, maxItems: 500, payload: { - environment: configs.ENVIRONMENT, + environment: utils.getEnvironment(), + client: { + javascript: { + source_map_enabled: true, + // This is only defined in production environments. + code_version: process.env.GIT_SHA, + guess_uncaught_frames: true, + }, + }, }, uncaughtErrorLevel: 'error', hostWhiteList: [configs.DOMAIN_PRODUCTION, configs.DOMAIN_STAGING], @@ -28,25 +36,18 @@ const rollbarConfig = { 'SecurityError (DOM Exception 18)', ], }; -import Rollbar = require('../../public/js/rollbar.umd.nojson.min.js'); +import Rollbar = require('../../public/js/rollbar.umd.min.js'); const rollbar = Rollbar.init(rollbarConfig); export const errorReporter = { - async reportAsync(err: Error): Promise<any> { - if (configs.ENVIRONMENT === Environments.DEVELOPMENT) { + report(err: Error): void { + if (utils.isDevelopment()) { return; // Let's not log development errors to rollbar } - - return new Promise((resolve, _reject) => { - rollbar.error(err, (rollbarErr: Error) => { - if (rollbarErr) { - logUtils.log(`Error reporting to rollbar, ignoring: ${rollbarErr}`); - // We never want to reject and cause the app to throw because of rollbar - resolve(); - } else { - resolve(); - } - }); + rollbar.error(err, (rollbarErr: Error) => { + if (rollbarErr) { + logUtils.log(`Error reporting to rollbar, ignoring: ${rollbarErr}`); + } }); }, }; diff --git a/packages/website/ts/utils/fetch_utils.ts b/packages/website/ts/utils/fetch_utils.ts index 513f7e479..e9a88b6b3 100644 --- a/packages/website/ts/utils/fetch_utils.ts +++ b/packages/website/ts/utils/fetch_utils.ts @@ -1,4 +1,4 @@ -import { logUtils } from '@0xproject/utils'; +import { fetchAsync, logUtils } from '@0xproject/utils'; import * as _ from 'lodash'; import * as queryString from 'query-string'; @@ -9,8 +9,7 @@ const logErrorIfPresent = (response: Response, requestedURL: string) => { const errorText = `Error requesting url: ${requestedURL}, ${response.status}: ${response.statusText}`; logUtils.log(errorText); const error = Error(errorText); - // tslint:disable-next-line:no-floating-promises - errorReporter.reportAsync(error); + errorReporter.report(error); throw error; } }; @@ -19,14 +18,14 @@ export const fetchUtils = { async requestAsync(baseUrl: string, path: string, queryParams?: object): Promise<any> { const query = queryStringFromQueryParams(queryParams); const url = `${baseUrl}${path}${query}`; - const response = await fetch(url); + const response = await fetchAsync(url); logErrorIfPresent(response, url); const result = await response.json(); return result; }, async postAsync(baseUrl: string, path: string, body: object): Promise<Response> { const url = `${baseUrl}${path}`; - const response = await fetch(url, { + const response = await fetchAsync(url, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/packages/website/ts/utils/utils.ts b/packages/website/ts/utils/utils.ts index 8c76a7592..e656d5963 100644 --- a/packages/website/ts/utils/utils.ts +++ b/packages/website/ts/utils/utils.ts @@ -30,8 +30,6 @@ import { configs } from 'ts/utils/configs'; import { constants } from 'ts/utils/constants'; import * as u2f from 'ts/vendor/u2f_api'; -const isDogfood = (): boolean => _.includes(window.location.href, configs.DOMAIN_DOGFOOD); - export const utils = { assert(condition: boolean, message: string): void { if (!condition) { @@ -61,7 +59,7 @@ export const utils = { return moment.unix(unixTimestampSec.toNumber()); }, convertToReadableDateTimeFromUnixTimestamp(unixTimestampSec: BigNumber): string { - const m = this.convertToMomentFromUnixTimestamp(unixTimestampSec); + const m = utils.convertToMomentFromUnixTimestamp(unixTimestampSec); const formattedDate: string = m.format('h:MMa MMMM D YYYY'); return formattedDate; }, @@ -177,18 +175,6 @@ export const utils = { _.includes(errMsg, ledgerDenialErrMsg); return isUserDeniedErrMsg; }, - getCurrentEnvironment(): string { - switch (location.host) { - case configs.DOMAIN_DEVELOPMENT: - return 'development'; - case configs.DOMAIN_STAGING: - return 'staging'; - case configs.DOMAIN_PRODUCTION: - return 'production'; - default: - return 'production'; - } - }, getAddressBeginAndEnd(address: string): string { const truncatedAddress = `${address.substring(0, 6)}...${address.substr(-4)}`; // 0x3d5a...b287 return truncatedAddress; @@ -313,14 +299,13 @@ export const utils = { const baseUrl = `https://${window.location.hostname}${hasPort ? `:${port}` : ''}`; return baseUrl; }, - async onPageLoadAsync(): Promise<void> { + onPageLoadPromise: new Promise<void>((resolve, _reject) => { if (document.readyState === 'complete') { - return; // Already loaded + resolve(); + return; } - return new Promise<void>((resolve, _reject) => { - window.onload = () => resolve(); - }); - }, + window.onload = () => resolve(); + }), getProviderType(provider: Provider): Providers | string { const constructorName = provider.constructor.name; let parsedProviderName = constructorName; @@ -346,10 +331,10 @@ export const utils = { return parsedProviderName; }, getBackendBaseUrl(): string { - return isDogfood() ? configs.BACKEND_BASE_STAGING_URL : configs.BACKEND_BASE_PROD_URL; + return utils.isDogfood() ? configs.BACKEND_BASE_STAGING_URL : configs.BACKEND_BASE_PROD_URL; }, isDevelopment(): boolean { - return configs.ENVIRONMENT === Environments.DEVELOPMENT; + return _.includes(configs.DOMAINS_DEVELOPMENT, window.location.origin); }, isStaging(): boolean { return _.includes(window.location.href, configs.DOMAIN_STAGING); @@ -357,14 +342,29 @@ export const utils = { isExternallyInjected(providerType: ProviderType, injectedProviderName: string): boolean { return providerType === ProviderType.Injected && injectedProviderName !== constants.PROVIDER_NAME_PUBLIC; }, - isDogfood, - shouldShowPortalV2(): boolean { - // return this.isDevelopment() || this.isStaging() || this.isDogfood(); - // TODO: Remove this method entirely after launch. - return true; + isDogfood(): boolean { + return _.includes(window.location.href, configs.DOMAIN_DOGFOOD); + }, + isProduction(): boolean { + return _.includes(window.location.href, configs.DOMAIN_PRODUCTION); + }, + getEnvironment(): Environments { + if (utils.isDogfood()) { + return Environments.DOGFOOD; + } + if (utils.isDevelopment()) { + return Environments.DEVELOPMENT; + } + if (utils.isStaging()) { + return Environments.STAGING; + } + if (utils.isProduction()) { + return Environments.PRODUCTION; + } + return Environments.UNKNOWN; }, shouldShowJobsPage(): boolean { - return this.isDevelopment() || this.isStaging() || this.isDogfood(); + return utils.isDevelopment() || utils.isStaging() || utils.isDogfood(); }, getEthToken(tokenByAddress: TokenByAddress): Token { return utils.getTokenBySymbol(constants.ETHER_TOKEN_SYMBOL, tokenByAddress); @@ -379,27 +379,34 @@ export const utils = { }, getTrackedTokens(tokenByAddress: TokenByAddress): Token[] { const allTokens = _.values(tokenByAddress); - const trackedTokens = _.filter(allTokens, t => this.isTokenTracked(t)); + const trackedTokens = _.filter(allTokens, t => utils.isTokenTracked(t)); return trackedTokens; }, getFormattedAmountFromToken(token: Token, tokenState: TokenState): string { return utils.getFormattedAmount(tokenState.balance, token.decimals); }, + format(value: BigNumber, format: string): string { + const formattedAmount = numeral(value).format(format); + if (_.isNaN(formattedAmount)) { + // https://github.com/adamwdraper/Numeral-js/issues/596 + return numeral(new BigNumber(0)).format(format); + } + return formattedAmount; + }, getFormattedAmount(amount: BigNumber, decimals: number): string { const unitAmount = Web3Wrapper.toUnitAmount(amount, decimals); // if the unit amount is less than 1, show the natural number of decimal places with a max of 4 // if the unit amount is greater than or equal to 1, show only 2 decimal places - const precision = unitAmount.lt(1) - ? Math.min(constants.TOKEN_AMOUNT_DISPLAY_PRECISION, unitAmount.decimalPlaces()) - : 2; + const lessThanOnePrecision = Math.min(constants.TOKEN_AMOUNT_DISPLAY_PRECISION, unitAmount.decimalPlaces()); + const greaterThanOnePrecision = 2; + const precision = unitAmount.lt(1) ? lessThanOnePrecision : greaterThanOnePrecision; const format = `0,0.${_.repeat('0', precision)}`; - const formattedAmount = numeral(unitAmount).format(format); - return formattedAmount; + return utils.format(unitAmount, format); }, getUsdValueFormattedAmount(amount: BigNumber, decimals: number, price: BigNumber): string { const unitAmount = Web3Wrapper.toUnitAmount(amount, decimals); const value = unitAmount.mul(price); - return numeral(value).format(constants.NUMERAL_USD_FORMAT); + return utils.format(value, constants.NUMERAL_USD_FORMAT); }, openUrl(url: string): void { window.open(url, '_blank'); |