Merge pull request #1424 from 0xProject/feature/instant/usd-eth-toggle

[instant] ETH/USD toggle
This commit is contained in:
Steve Klebanoff 2018-12-14 17:33:56 -08:00 committed by GitHub
commit 737d1dc54d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 316 additions and 118 deletions

View File

@ -113,20 +113,23 @@ export class InstantHeading extends React.Component<InstantHeadingProps, {}> {
} }
private readonly _renderEthAmount = (): React.ReactNode => { private readonly _renderEthAmount = (): React.ReactNode => {
const ethAmount = format.ethBaseUnitAmount(
this.props.totalEthBaseUnitAmount,
4,
<AmountPlaceholder isPulsating={false} color={PLACEHOLDER_COLOR} />,
);
const fontSize = _.isString(ethAmount) && ethAmount.length >= 13 ? '14px' : '16px';
return ( return (
<Text <Text
fontSize="16px" fontSize={fontSize}
textAlign="right" textAlign="right"
width="100%" width="100%"
fontColor={ColorOption.white} fontColor={ColorOption.white}
fontWeight={500} fontWeight={500}
noWrap={true} noWrap={true}
> >
{format.ethBaseUnitAmount( {ethAmount}
this.props.totalEthBaseUnitAmount,
4,
<AmountPlaceholder isPulsating={false} color={PLACEHOLDER_COLOR} />,
)}
</Text> </Text>
); );
}; };

View File

@ -4,124 +4,227 @@ import * as _ from 'lodash';
import * as React from 'react'; import * as React from 'react';
import { oc } from 'ts-optchain'; import { oc } from 'ts-optchain';
import { BIG_NUMBER_ZERO } from '../constants'; import { BIG_NUMBER_ZERO, DEFAULT_UNKOWN_ASSET_NAME } from '../constants';
import { ColorOption } from '../style/theme'; import { ColorOption } from '../style/theme';
import { BaseCurrency } from '../types';
import { format } from '../util/format'; import { format } from '../util/format';
import { AmountPlaceholder } from './amount_placeholder'; import { AmountPlaceholder } from './amount_placeholder';
import { SectionHeader } from './section_header';
import { Container } from './ui/container'; import { Container } from './ui/container';
import { Flex } from './ui/flex'; import { Flex } from './ui/flex';
import { Text } from './ui/text'; import { Text, TextProps } from './ui/text';
export interface OrderDetailsProps { export interface OrderDetailsProps {
buyQuoteInfo?: BuyQuoteInfo; buyQuoteInfo?: BuyQuoteInfo;
selectedAssetUnitAmount?: BigNumber; selectedAssetUnitAmount?: BigNumber;
ethUsdPrice?: BigNumber; ethUsdPrice?: BigNumber;
isLoading: boolean; isLoading: boolean;
assetName?: string;
baseCurrency: BaseCurrency;
onBaseCurrencySwitchEth: () => void;
onBaseCurrencySwitchUsd: () => void;
} }
export class OrderDetails extends React.Component<OrderDetailsProps> { export class OrderDetails extends React.Component<OrderDetailsProps> {
public render(): React.ReactNode { public render(): React.ReactNode {
const { buyQuoteInfo, ethUsdPrice, selectedAssetUnitAmount } = this.props; const shouldShowUsdError = this.props.baseCurrency === BaseCurrency.USD && this._hadErrorFetchingUsdPrice();
const buyQuoteAccessor = oc(buyQuoteInfo);
const assetEthBaseUnitAmount = buyQuoteAccessor.assetEthAmount();
const feeEthBaseUnitAmount = buyQuoteAccessor.feeEthAmount();
const totalEthBaseUnitAmount = buyQuoteAccessor.totalEthAmount();
const pricePerTokenEth =
!_.isUndefined(assetEthBaseUnitAmount) &&
!_.isUndefined(selectedAssetUnitAmount) &&
!selectedAssetUnitAmount.eq(BIG_NUMBER_ZERO)
? assetEthBaseUnitAmount.div(selectedAssetUnitAmount).ceil()
: undefined;
return ( return (
<Container width="100%" flexGrow={1} padding="20px 20px 0px 20px"> <Container width="100%" flexGrow={1} padding="20px 20px 0px 20px">
<Container marginBottom="10px"> <Container marginBottom="10px">{this._renderHeader()}</Container>
<Text {shouldShowUsdError ? this._renderErrorFetchingUsdPrice() : this._renderRows()}
letterSpacing="1px"
fontColor={ColorOption.primaryColor}
fontWeight={600}
textTransform="uppercase"
fontSize="14px"
>
Order Details
</Text>
</Container>
<EthAmountRow
rowLabel="Token Price"
ethAmount={pricePerTokenEth}
ethUsdPrice={ethUsdPrice}
isLoading={this.props.isLoading}
/>
<EthAmountRow
rowLabel="Fee"
ethAmount={feeEthBaseUnitAmount}
ethUsdPrice={ethUsdPrice}
isLoading={this.props.isLoading}
/>
<EthAmountRow
rowLabel="Total Cost"
ethAmount={totalEthBaseUnitAmount}
ethUsdPrice={ethUsdPrice}
shouldEmphasize={true}
isLoading={this.props.isLoading}
/>
</Container> </Container>
); );
} }
private _renderRows(): React.ReactNode {
const { buyQuoteInfo } = this.props;
return (
<React.Fragment>
<OrderDetailsRow
labelText={this._assetAmountLabel()}
primaryValue={this._displayAmountOrPlaceholder(buyQuoteInfo && buyQuoteInfo.assetEthAmount)}
/>
<OrderDetailsRow
labelText="Fee"
primaryValue={this._displayAmountOrPlaceholder(buyQuoteInfo && buyQuoteInfo.feeEthAmount)}
/>
<OrderDetailsRow
labelText="Total Cost"
isLabelBold={true}
primaryValue={this._displayAmountOrPlaceholder(buyQuoteInfo && buyQuoteInfo.totalEthAmount)}
isPrimaryValueBold={true}
secondaryValue={this._totalCostSecondaryValue()}
/>
</React.Fragment>
);
}
private _renderErrorFetchingUsdPrice(): React.ReactNode {
return (
<Text>
There was an error fetching the USD price.
<Text
onClick={this.props.onBaseCurrencySwitchEth}
fontWeight={700}
fontColor={ColorOption.primaryColor}
>
Click here
</Text>
{' to view ETH prices'}
</Text>
);
}
private _hadErrorFetchingUsdPrice(): boolean {
return this.props.ethUsdPrice ? this.props.ethUsdPrice.equals(BIG_NUMBER_ZERO) : false;
}
private _totalCostSecondaryValue(): React.ReactNode {
const secondaryCurrency = this.props.baseCurrency === BaseCurrency.USD ? BaseCurrency.ETH : BaseCurrency.USD;
const canDisplayCurrency =
secondaryCurrency === BaseCurrency.ETH ||
(secondaryCurrency === BaseCurrency.USD && this.props.ethUsdPrice && !this._hadErrorFetchingUsdPrice());
if (this.props.buyQuoteInfo && canDisplayCurrency) {
return this._displayAmount(secondaryCurrency, this.props.buyQuoteInfo.totalEthAmount);
} else {
return undefined;
}
}
private _displayAmountOrPlaceholder(weiAmount?: BigNumber): React.ReactNode {
const { baseCurrency, isLoading } = this.props;
if (_.isUndefined(weiAmount)) {
return (
<Container opacity={0.5}>
<AmountPlaceholder color={ColorOption.lightGrey} isPulsating={isLoading} />
</Container>
);
}
return this._displayAmount(baseCurrency, weiAmount);
}
private _displayAmount(currency: BaseCurrency, weiAmount: BigNumber): React.ReactNode {
switch (currency) {
case BaseCurrency.USD:
return format.ethBaseUnitAmountInUsd(weiAmount, this.props.ethUsdPrice, 2, '');
case BaseCurrency.ETH:
return format.ethBaseUnitAmount(weiAmount, 4, '');
}
}
private _assetAmountLabel(): React.ReactNode {
const { assetName, baseCurrency } = this.props;
const numTokens = this.props.selectedAssetUnitAmount;
// Display as 0 if we have a selected asset
const displayNumTokens =
assetName && assetName !== DEFAULT_UNKOWN_ASSET_NAME && _.isUndefined(numTokens)
? new BigNumber(0)
: numTokens;
if (!_.isUndefined(displayNumTokens)) {
let numTokensWithSymbol: React.ReactNode = displayNumTokens.toString();
if (assetName) {
numTokensWithSymbol += ` ${assetName}`;
}
const pricePerTokenWei = this._pricePerTokenWei();
if (pricePerTokenWei) {
const atPriceDisplay = (
<Text fontColor={ColorOption.lightGrey}>
@ {this._displayAmount(baseCurrency, pricePerTokenWei)}
</Text>
);
numTokensWithSymbol = (
<React.Fragment>
{numTokensWithSymbol} {atPriceDisplay}
</React.Fragment>
);
}
return numTokensWithSymbol;
}
return 'Token Amount';
}
private _pricePerTokenWei(): BigNumber | undefined {
const buyQuoteAccessor = oc(this.props.buyQuoteInfo);
const assetTotalInWei = buyQuoteAccessor.assetEthAmount();
const selectedAssetUnitAmount = this.props.selectedAssetUnitAmount;
return !_.isUndefined(assetTotalInWei) &&
!_.isUndefined(selectedAssetUnitAmount) &&
!selectedAssetUnitAmount.eq(BIG_NUMBER_ZERO)
? assetTotalInWei.div(selectedAssetUnitAmount).ceil()
: undefined;
}
private _baseCurrencyChoice(choice: BaseCurrency): React.ReactNode {
const onClick =
choice === BaseCurrency.ETH ? this.props.onBaseCurrencySwitchEth : this.props.onBaseCurrencySwitchUsd;
const isSelected = this.props.baseCurrency === choice;
const textStyle: TextProps = { onClick, fontSize: '12px' };
if (isSelected) {
textStyle.fontColor = ColorOption.primaryColor;
textStyle.fontWeight = 700;
} else {
textStyle.fontColor = ColorOption.lightGrey;
}
return <Text {...textStyle}>{choice}</Text>;
}
private _renderHeader(): React.ReactNode {
return (
<Flex justify="space-between">
<SectionHeader>Order Details</SectionHeader>
<Container>
{this._baseCurrencyChoice(BaseCurrency.ETH)}
<Container marginLeft="5px" marginRight="5px" display="inline">
<Text fontSize="12px" fontColor={ColorOption.feintGrey}>
/
</Text>
</Container>
{this._baseCurrencyChoice(BaseCurrency.USD)}
</Container>
</Flex>
);
}
} }
export interface EthAmountRowProps { export interface OrderDetailsRowProps {
rowLabel: string; labelText: React.ReactNode;
ethAmount?: BigNumber; isLabelBold?: boolean;
isEthAmountInBaseUnits?: boolean; isPrimaryValueBold?: boolean;
ethUsdPrice?: BigNumber; primaryValue: React.ReactNode;
shouldEmphasize?: boolean; secondaryValue?: React.ReactNode;
isLoading: boolean;
} }
export class OrderDetailsRow extends React.Component<OrderDetailsRowProps, {}> {
export class EthAmountRow extends React.Component<EthAmountRowProps> {
public static defaultProps = {
shouldEmphasize: false,
isEthAmountInBaseUnits: true,
};
public render(): React.ReactNode { public render(): React.ReactNode {
const { rowLabel, ethAmount, isEthAmountInBaseUnits, shouldEmphasize, isLoading } = this.props;
const fontWeight = shouldEmphasize ? 700 : 400;
const ethFormatter = isEthAmountInBaseUnits ? format.ethBaseUnitAmount : format.ethUnitAmount;
return ( return (
<Container padding="10px 0px" borderTop="1px dashed" borderColor={ColorOption.feintGrey}> <Container padding="10px 0px" borderTop="1px dashed" borderColor={ColorOption.feintGrey}>
<Flex justify="space-between"> <Flex justify="space-between">
<Text fontWeight={fontWeight} fontColor={ColorOption.grey}> <Text fontWeight={this.props.isLabelBold ? 700 : 400} fontColor={ColorOption.grey}>
{rowLabel} {this.props.labelText}
</Text> </Text>
<Container> <Container>{this._renderValues()}</Container>
{this._renderUsdSection()}
<Text fontWeight={fontWeight} fontColor={ColorOption.grey}>
{ethFormatter(
ethAmount,
4,
<Container opacity={0.5}>
<AmountPlaceholder color={ColorOption.lightGrey} isPulsating={isLoading} />
</Container>,
)}
</Text>
</Container>
</Flex> </Flex>
</Container> </Container>
); );
} }
private _renderUsdSection(): React.ReactNode {
const usdFormatter = this.props.isEthAmountInBaseUnits private _renderValues(): React.ReactNode {
? format.ethBaseUnitAmountInUsd const secondaryValueNode: React.ReactNode = this.props.secondaryValue && (
: format.ethUnitAmountInUsd;
const shouldHideUsdPriceSection = _.isUndefined(this.props.ethUsdPrice) || _.isUndefined(this.props.ethAmount);
return shouldHideUsdPriceSection ? null : (
<Container marginRight="3px" display="inline-block"> <Container marginRight="3px" display="inline-block">
<Text fontColor={ColorOption.lightGrey}> <Text fontColor={ColorOption.lightGrey}>({this.props.secondaryValue})</Text>
({usdFormatter(this.props.ethAmount, this.props.ethUsdPrice)})
</Text>
</Container> </Container>
); );
return (
<React.Fragment>
{secondaryValueNode}
<Text fontWeight={this.props.isPrimaryValueBold ? 700 : 400}>{this.props.primaryValue}</Text>
</React.Fragment>
);
} }
} }

View File

@ -8,6 +8,7 @@ import { envUtil } from '../util/env';
import { CoinbaseWalletLogo } from './coinbase_wallet_logo'; import { CoinbaseWalletLogo } from './coinbase_wallet_logo';
import { MetaMaskLogo } from './meta_mask_logo'; import { MetaMaskLogo } from './meta_mask_logo';
import { PaymentMethodDropdown } from './payment_method_dropdown'; import { PaymentMethodDropdown } from './payment_method_dropdown';
import { SectionHeader } from './section_header';
import { Circle } from './ui/circle'; import { Circle } from './ui/circle';
import { Container } from './ui/container'; import { Container } from './ui/container';
import { Flex } from './ui/flex'; import { Flex } from './ui/flex';
@ -29,15 +30,7 @@ export class PaymentMethod extends React.Component<PaymentMethodProps> {
<Container width="100%" height="120px" padding="20px 20px 0px 20px"> <Container width="100%" height="120px" padding="20px 20px 0px 20px">
<Container marginBottom="12px"> <Container marginBottom="12px">
<Flex justify="space-between"> <Flex justify="space-between">
<Text <SectionHeader>{this._renderTitleText()}</SectionHeader>
letterSpacing="1px"
fontColor={ColorOption.primaryColor}
fontWeight={600}
textTransform="uppercase"
fontSize="14px"
>
{this._renderTitleText()}
</Text>
{this._renderTitleLabel()} {this._renderTitleLabel()}
</Flex> </Flex>
</Container> </Container>

View File

@ -0,0 +1,20 @@
import * as React from 'react';
import { ColorOption } from '../style/theme';
import { Text } from './ui/text';
export interface SectionHeaderProps {}
export const SectionHeader: React.StatelessComponent<SectionHeaderProps> = props => {
return (
<Text
letterSpacing="1px"
fontColor={ColorOption.primaryColor}
fontWeight={600}
textTransform="uppercase"
fontSize="12px"
>
{props.children}
</Text>
);
};

View File

@ -122,6 +122,7 @@ export class ZeroExInstantProvider extends React.Component<ZeroExInstantProvider
window, window,
state.selectedAsset, state.selectedAsset,
this.props.affiliateInfo, this.props.affiliateInfo,
state.baseCurrency,
), ),
); );
analytics.trackInstantOpened(); analytics.trackInstantOpened();

View File

@ -17,6 +17,7 @@ export const ONE_MINUTE_MS = ONE_SECOND_MS * 60;
export const GIT_SHA = process.env.GIT_SHA; export const GIT_SHA = process.env.GIT_SHA;
export const NODE_ENV = process.env.NODE_ENV; export const NODE_ENV = process.env.NODE_ENV;
export const NPM_PACKAGE_VERSION = process.env.NPM_PACKAGE_VERSION; export const NPM_PACKAGE_VERSION = process.env.NPM_PACKAGE_VERSION;
export const DEFAULT_UNKOWN_ASSET_NAME = '???';
export const ACCOUNT_UPDATE_INTERVAL_TIME_MS = ONE_SECOND_MS * 5; export const ACCOUNT_UPDATE_INTERVAL_TIME_MS = ONE_SECOND_MS * 5;
export const BUY_QUOTE_UPDATE_INTERVAL_TIME_MS = ONE_SECOND_MS * 15; export const BUY_QUOTE_UPDATE_INTERVAL_TIME_MS = ONE_SECOND_MS * 15;
export const DEFAULT_GAS_PRICE = GWEI_IN_WEI.mul(6); export const DEFAULT_GAS_PRICE = GWEI_IN_WEI.mul(6);

View File

@ -1,32 +1,41 @@
import { BuyQuoteInfo } from '@0x/asset-buyer';
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash'; import * as _ from 'lodash';
import * as React from 'react'; import * as React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Dispatch } from 'redux';
import { oc } from 'ts-optchain'; import { oc } from 'ts-optchain';
import { Action, actions } from '../redux/actions';
import { State } from '../redux/reducer'; import { State } from '../redux/reducer';
import { OrderDetails } from '../components/order_details'; import { OrderDetails, OrderDetailsProps } from '../components/order_details';
import { AsyncProcessState } from '../types'; import { AsyncProcessState, BaseCurrency, Omit } from '../types';
import { assetUtils } from '../util/asset';
export interface LatestBuyQuoteOrderDetailsProps {} type DispatchProperties = 'onBaseCurrencySwitchEth' | 'onBaseCurrencySwitchUsd';
interface ConnectedState {
buyQuoteInfo?: BuyQuoteInfo;
selectedAssetUnitAmount?: BigNumber;
ethUsdPrice?: BigNumber;
isLoading: boolean;
}
interface ConnectedState extends Omit<OrderDetailsProps, DispatchProperties> {}
const mapStateToProps = (state: State, _ownProps: LatestBuyQuoteOrderDetailsProps): ConnectedState => ({ const mapStateToProps = (state: State, _ownProps: LatestBuyQuoteOrderDetailsProps): ConnectedState => ({
// use the worst case quote info // use the worst case quote info
buyQuoteInfo: oc(state).latestBuyQuote.worstCaseQuoteInfo(), buyQuoteInfo: oc(state).latestBuyQuote.worstCaseQuoteInfo(),
selectedAssetUnitAmount: state.selectedAssetUnitAmount, selectedAssetUnitAmount: state.selectedAssetUnitAmount,
ethUsdPrice: state.ethUsdPrice, ethUsdPrice: state.ethUsdPrice,
isLoading: state.quoteRequestState === AsyncProcessState.Pending, isLoading: state.quoteRequestState === AsyncProcessState.Pending,
assetName: assetUtils.bestNameForAsset(state.selectedAsset),
baseCurrency: state.baseCurrency,
}); });
interface ConnectedDispatch extends Pick<OrderDetailsProps, DispatchProperties> {}
const mapDispatchToProps = (dispatch: Dispatch<Action>): ConnectedDispatch => ({
onBaseCurrencySwitchEth: () => {
dispatch(actions.updateBaseCurrency(BaseCurrency.ETH));
},
onBaseCurrencySwitchUsd: () => {
dispatch(actions.updateBaseCurrency(BaseCurrency.USD));
},
});
export interface LatestBuyQuoteOrderDetailsProps {}
export const LatestBuyQuoteOrderDetails: React.ComponentClass<LatestBuyQuoteOrderDetailsProps> = connect( export const LatestBuyQuoteOrderDetails: React.ComponentClass<LatestBuyQuoteOrderDetailsProps> = connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps,
)(OrderDetails); )(OrderDetails);

View File

@ -2,7 +2,7 @@ import { BuyQuote } from '@0x/asset-buyer';
import { BigNumber } from '@0x/utils'; import { BigNumber } from '@0x/utils';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { ActionsUnion, AddressAndEthBalanceInWei, Asset, StandardSlidingPanelContent } from '../types'; import { ActionsUnion, AddressAndEthBalanceInWei, Asset, BaseCurrency, StandardSlidingPanelContent } from '../types';
export interface PlainAction<T extends string> { export interface PlainAction<T extends string> {
type: T; type: T;
@ -43,6 +43,7 @@ export enum ActionTypes {
RESET_AMOUNT = 'RESET_AMOUNT', RESET_AMOUNT = 'RESET_AMOUNT',
OPEN_STANDARD_SLIDING_PANEL = 'OPEN_STANDARD_SLIDING_PANEL', OPEN_STANDARD_SLIDING_PANEL = 'OPEN_STANDARD_SLIDING_PANEL',
CLOSE_STANDARD_SLIDING_PANEL = 'CLOSE_STANDARD_SLIDING_PANEL', CLOSE_STANDARD_SLIDING_PANEL = 'CLOSE_STANDARD_SLIDING_PANEL',
UPDATE_BASE_CURRENCY = 'UPDATE_BASE_CURRENCY',
} }
export const actions = { export const actions = {
@ -72,4 +73,5 @@ export const actions = {
openStandardSlidingPanel: (content: StandardSlidingPanelContent) => openStandardSlidingPanel: (content: StandardSlidingPanelContent) =>
createAction(ActionTypes.OPEN_STANDARD_SLIDING_PANEL, content), createAction(ActionTypes.OPEN_STANDARD_SLIDING_PANEL, content),
closeStandardSlidingPanel: () => createAction(ActionTypes.CLOSE_STANDARD_SLIDING_PANEL), closeStandardSlidingPanel: () => createAction(ActionTypes.CLOSE_STANDARD_SLIDING_PANEL),
updateBaseCurrency: (baseCurrency: BaseCurrency) => createAction(ActionTypes.UPDATE_BASE_CURRENCY, baseCurrency),
}; };

View File

@ -99,6 +99,9 @@ export const analyticsMiddleware: Middleware = store => next => middlewareAction
analytics.trackInstallWalletModalClosed(); analytics.trackInstallWalletModalClosed();
} }
break; break;
case ActionTypes.UPDATE_BASE_CURRENCY:
analytics.trackBaseCurrencyChanged(curState.baseCurrency);
analytics.addEventProperties({ baseCurrency: curState.baseCurrency });
} }
return nextAction; return nextAction;

View File

@ -4,7 +4,7 @@ import * as _ from 'lodash';
import { Dispatch } from 'redux'; import { Dispatch } from 'redux';
import { BIG_NUMBER_ZERO } from '../constants'; import { BIG_NUMBER_ZERO } from '../constants';
import { AccountState, ERC20Asset, OrderProcessState, ProviderState, QuoteFetchOrigin } from '../types'; import { AccountState, BaseCurrency, ERC20Asset, OrderProcessState, ProviderState, QuoteFetchOrigin } from '../types';
import { analytics } from '../util/analytics'; import { analytics } from '../util/analytics';
import { assetUtils } from '../util/asset'; import { assetUtils } from '../util/asset';
import { buyQuoteUpdater } from '../util/buy_quote_updater'; import { buyQuoteUpdater } from '../util/buy_quote_updater';
@ -24,7 +24,9 @@ export const asyncData = {
const errorMessage = 'Error fetching ETH/USD price'; const errorMessage = 'Error fetching ETH/USD price';
errorFlasher.flashNewErrorMessage(dispatch, errorMessage); errorFlasher.flashNewErrorMessage(dispatch, errorMessage);
dispatch(actions.updateEthUsdPrice(BIG_NUMBER_ZERO)); dispatch(actions.updateEthUsdPrice(BIG_NUMBER_ZERO));
dispatch(actions.updateBaseCurrency(BaseCurrency.ETH));
errorReporter.report(e); errorReporter.report(e);
analytics.trackUsdPriceFailed();
} }
}, },
fetchAvailableAssetDatasAndDispatchToStore: async (state: State, dispatch: Dispatch) => { fetchAvailableAssetDatasAndDispatchToStore: async (state: State, dispatch: Dispatch) => {

View File

@ -14,6 +14,7 @@ import {
Asset, Asset,
AssetMetaData, AssetMetaData,
AsyncProcessState, AsyncProcessState,
BaseCurrency,
DisplayStatus, DisplayStatus,
Network, Network,
OrderProcessState, OrderProcessState,
@ -33,6 +34,7 @@ export interface DefaultState {
latestErrorDisplayStatus: DisplayStatus; latestErrorDisplayStatus: DisplayStatus;
quoteRequestState: AsyncProcessState; quoteRequestState: AsyncProcessState;
standardSlidingPanelSettings: StandardSlidingPanelSettings; standardSlidingPanelSettings: StandardSlidingPanelSettings;
baseCurrency: BaseCurrency;
} }
// State that is required but needs to be derived from the props // State that is required but needs to be derived from the props
@ -64,6 +66,7 @@ export const DEFAULT_STATE: DefaultState = {
animationState: 'none', animationState: 'none',
content: StandardSlidingPanelContent.None, content: StandardSlidingPanelContent.None,
}, },
baseCurrency: BaseCurrency.USD,
}; };
export const createReducer = (initialState: State) => { export const createReducer = (initialState: State) => {
@ -243,6 +246,11 @@ export const createReducer = (initialState: State) => {
animationState: 'slidOut', animationState: 'slidOut',
}, },
}; };
case ActionTypes.UPDATE_BASE_CURRENCY:
return {
...state,
baseCurrency: action.data,
};
default: default:
return state; return state;
} }

View File

@ -26,6 +26,11 @@ export enum QuoteFetchOrigin {
Heartbeat = 'Heartbeat', Heartbeat = 'Heartbeat',
} }
export enum BaseCurrency {
USD = 'USD',
ETH = 'ETH',
}
export interface SimulatedProgress { export interface SimulatedProgress {
startTimeUnix: number; startTimeUnix: number;
expectedEndTimeUnix: number; expectedEndTimeUnix: number;

View File

@ -6,6 +6,7 @@ import { GIT_SHA, HEAP_ENABLED, INSTANT_DISCHARGE_TARGET, NODE_ENV, NPM_PACKAGE_
import { import {
AffiliateInfo, AffiliateInfo,
Asset, Asset,
BaseCurrency,
Network, Network,
OrderProcessState, OrderProcessState,
OrderSource, OrderSource,
@ -37,6 +38,7 @@ enum EventNames {
ACCOUNT_UNLOCK_REQUESTED = 'Account - Unlock Requested', ACCOUNT_UNLOCK_REQUESTED = 'Account - Unlock Requested',
ACCOUNT_UNLOCK_DENIED = 'Account - Unlock Denied', ACCOUNT_UNLOCK_DENIED = 'Account - Unlock Denied',
ACCOUNT_ADDRESS_CHANGED = 'Account - Address Changed', ACCOUNT_ADDRESS_CHANGED = 'Account - Address Changed',
BASE_CURRENCY_CHANGED = 'Base Currency - Changed',
PAYMENT_METHOD_DROPDOWN_OPENED = 'Payment Method - Dropdown Opened', PAYMENT_METHOD_DROPDOWN_OPENED = 'Payment Method - Dropdown Opened',
PAYMENT_METHOD_OPENED_ETHERSCAN = 'Payment Method - Opened Etherscan', PAYMENT_METHOD_OPENED_ETHERSCAN = 'Payment Method - Opened Etherscan',
PAYMENT_METHOD_COPIED_ADDRESS = 'Payment Method - Copied Address', PAYMENT_METHOD_COPIED_ADDRESS = 'Payment Method - Copied Address',
@ -47,6 +49,7 @@ enum EventNames {
BUY_TX_SUBMITTED = 'Buy - Tx Submitted', BUY_TX_SUBMITTED = 'Buy - Tx Submitted',
BUY_TX_SUCCEEDED = 'Buy - Tx Succeeded', BUY_TX_SUCCEEDED = 'Buy - Tx Succeeded',
BUY_TX_FAILED = 'Buy - Tx Failed', BUY_TX_FAILED = 'Buy - Tx Failed',
USD_PRICE_FETCH_FAILED = 'USD Price - Fetch Failed',
INSTALL_WALLET_CLICKED = 'Install Wallet - Clicked', INSTALL_WALLET_CLICKED = 'Install Wallet - Clicked',
INSTALL_WALLET_MODAL_OPENED = 'Install Wallet - Modal - Opened', INSTALL_WALLET_MODAL_OPENED = 'Install Wallet - Modal - Opened',
INSTALL_WALLET_MODAL_CLICKED_EXPLANATION = 'Install Wallet - Modal - Clicked Explanation', INSTALL_WALLET_MODAL_CLICKED_EXPLANATION = 'Install Wallet - Modal - Clicked Explanation',
@ -118,6 +121,7 @@ export interface AnalyticsEventOptions {
selectedAssetSymbol?: string; selectedAssetSymbol?: string;
selectedAssetData?: string; selectedAssetData?: string;
selectedAssetDecimals?: number; selectedAssetDecimals?: number;
baseCurrency?: string;
} }
export enum TokenSelectorClosedVia { export enum TokenSelectorClosedVia {
ClickedX = 'Clicked X', ClickedX = 'Clicked X',
@ -141,6 +145,7 @@ export const analytics = {
window: Window, window: Window,
selectedAsset?: Asset, selectedAsset?: Asset,
affiliateInfo?: AffiliateInfo, affiliateInfo?: AffiliateInfo,
baseCurrency?: BaseCurrency,
): AnalyticsEventOptions => { ): AnalyticsEventOptions => {
const affiliateAddress = affiliateInfo ? affiliateInfo.feeRecipient : 'none'; const affiliateAddress = affiliateInfo ? affiliateInfo.feeRecipient : 'none';
const affiliateFeePercent = affiliateInfo ? parseFloat(affiliateInfo.feePercentage.toFixed(4)) : 0; const affiliateFeePercent = affiliateInfo ? parseFloat(affiliateInfo.feePercentage.toFixed(4)) : 0;
@ -159,6 +164,7 @@ export const analytics = {
selectedAssetName: selectedAsset ? selectedAsset.metaData.name : 'none', selectedAssetName: selectedAsset ? selectedAsset.metaData.name : 'none',
selectedAssetData: selectedAsset ? selectedAsset.assetData : 'none', selectedAssetData: selectedAsset ? selectedAsset.assetData : 'none',
instantEnvironment: INSTANT_DISCHARGE_TARGET || `Local ${NODE_ENV}`, instantEnvironment: INSTANT_DISCHARGE_TARGET || `Local ${NODE_ENV}`,
baseCurrency,
}; };
return eventOptions; return eventOptions;
}, },
@ -170,6 +176,8 @@ export const analytics = {
trackAccountUnlockDenied: trackingEventFnWithoutPayload(EventNames.ACCOUNT_UNLOCK_DENIED), trackAccountUnlockDenied: trackingEventFnWithoutPayload(EventNames.ACCOUNT_UNLOCK_DENIED),
trackAccountAddressChanged: (address: string) => trackAccountAddressChanged: (address: string) =>
trackingEventFnWithPayload(EventNames.ACCOUNT_ADDRESS_CHANGED)({ address }), trackingEventFnWithPayload(EventNames.ACCOUNT_ADDRESS_CHANGED)({ address }),
trackBaseCurrencyChanged: (currencyChangedTo: BaseCurrency) =>
trackingEventFnWithPayload(EventNames.BASE_CURRENCY_CHANGED)({ currencyChangedTo }),
trackPaymentMethodDropdownOpened: trackingEventFnWithoutPayload(EventNames.PAYMENT_METHOD_DROPDOWN_OPENED), trackPaymentMethodDropdownOpened: trackingEventFnWithoutPayload(EventNames.PAYMENT_METHOD_DROPDOWN_OPENED),
trackPaymentMethodOpenedEtherscan: trackingEventFnWithoutPayload(EventNames.PAYMENT_METHOD_OPENED_ETHERSCAN), trackPaymentMethodOpenedEtherscan: trackingEventFnWithoutPayload(EventNames.PAYMENT_METHOD_OPENED_ETHERSCAN),
trackPaymentMethodCopiedAddress: trackingEventFnWithoutPayload(EventNames.PAYMENT_METHOD_COPIED_ADDRESS), trackPaymentMethodCopiedAddress: trackingEventFnWithoutPayload(EventNames.PAYMENT_METHOD_COPIED_ADDRESS),
@ -230,4 +238,5 @@ export const analytics = {
fetchOrigin, fetchOrigin,
}); });
}, },
trackUsdPriceFailed: trackingEventFnWithoutPayload(EventNames.USD_PRICE_FETCH_FAILED),
}; };

View File

@ -2,6 +2,7 @@ import { AssetBuyerError } from '@0x/asset-buyer';
import { AssetProxyId, ObjectMap } from '@0x/types'; import { AssetProxyId, ObjectMap } from '@0x/types';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { DEFAULT_UNKOWN_ASSET_NAME } from '../constants';
import { assetDataNetworkMapping } from '../data/asset_data_network_mapping'; import { assetDataNetworkMapping } from '../data/asset_data_network_mapping';
import { Asset, AssetMetaData, ERC20Asset, Network, ZeroExInstantError } from '../types'; import { Asset, AssetMetaData, ERC20Asset, Network, ZeroExInstantError } from '../types';
@ -71,7 +72,7 @@ export const assetUtils = {
} }
return metaData; return metaData;
}, },
bestNameForAsset: (asset?: Asset, defaultName: string = '???'): string => { bestNameForAsset: (asset?: Asset, defaultName: string = DEFAULT_UNKOWN_ASSET_NAME): string => {
if (_.isUndefined(asset)) { if (_.isUndefined(asset)) {
return defaultName; return defaultName;
} }

View File

@ -2,7 +2,7 @@ import { BigNumber } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper'; import { Web3Wrapper } from '@0x/web3-wrapper';
import * as _ from 'lodash'; import * as _ from 'lodash';
import { ETH_DECIMALS } from '../constants'; import { BIG_NUMBER_ZERO, ETH_DECIMALS } from '../constants';
export const format = { export const format = {
ethBaseUnitAmount: ( ethBaseUnitAmount: (
@ -20,24 +20,38 @@ export const format = {
ethUnitAmount?: BigNumber, ethUnitAmount?: BigNumber,
decimalPlaces: number = 4, decimalPlaces: number = 4,
defaultText: React.ReactNode = '0 ETH', defaultText: React.ReactNode = '0 ETH',
minUnitAmountToDisplay: BigNumber = new BigNumber('0.00001'),
): React.ReactNode => { ): React.ReactNode => {
if (_.isUndefined(ethUnitAmount)) { if (_.isUndefined(ethUnitAmount)) {
return defaultText; return defaultText;
} }
const roundedAmount = ethUnitAmount.round(decimalPlaces).toDigits(decimalPlaces); let roundedAmount = ethUnitAmount.round(decimalPlaces).toDigits(decimalPlaces);
return `${roundedAmount} ETH`;
if (roundedAmount.eq(BIG_NUMBER_ZERO) && ethUnitAmount.greaterThan(BIG_NUMBER_ZERO)) {
// Sometimes for small ETH amounts (i.e. 0.000045) the amount rounded to 4 decimalPlaces is 0
// If that is the case, show to 1 significant digit
roundedAmount = new BigNumber(ethUnitAmount.toPrecision(1));
}
const displayAmount =
roundedAmount.greaterThan(BIG_NUMBER_ZERO) && roundedAmount.lessThan(minUnitAmountToDisplay)
? `< ${minUnitAmountToDisplay.toString()}`
: roundedAmount.toString();
return `${displayAmount} ETH`;
}, },
ethBaseUnitAmountInUsd: ( ethBaseUnitAmountInUsd: (
ethBaseUnitAmount?: BigNumber, ethBaseUnitAmount?: BigNumber,
ethUsdPrice?: BigNumber, ethUsdPrice?: BigNumber,
decimalPlaces: number = 2, decimalPlaces: number = 2,
defaultText: React.ReactNode = '$0.00', defaultText: React.ReactNode = '$0.00',
minUnitAmountToDisplay: BigNumber = new BigNumber('0.00001'),
): React.ReactNode => { ): React.ReactNode => {
if (_.isUndefined(ethBaseUnitAmount) || _.isUndefined(ethUsdPrice)) { if (_.isUndefined(ethBaseUnitAmount) || _.isUndefined(ethUsdPrice)) {
return defaultText; return defaultText;
} }
const ethUnitAmount = Web3Wrapper.toUnitAmount(ethBaseUnitAmount, ETH_DECIMALS); const ethUnitAmount = Web3Wrapper.toUnitAmount(ethBaseUnitAmount, ETH_DECIMALS);
return format.ethUnitAmountInUsd(ethUnitAmount, ethUsdPrice, decimalPlaces); return format.ethUnitAmountInUsd(ethUnitAmount, ethUsdPrice, decimalPlaces, minUnitAmountToDisplay);
}, },
ethUnitAmountInUsd: ( ethUnitAmountInUsd: (
ethUnitAmount?: BigNumber, ethUnitAmount?: BigNumber,
@ -48,7 +62,13 @@ export const format = {
if (_.isUndefined(ethUnitAmount) || _.isUndefined(ethUsdPrice)) { if (_.isUndefined(ethUnitAmount) || _.isUndefined(ethUsdPrice)) {
return defaultText; return defaultText;
} }
return `$${ethUnitAmount.mul(ethUsdPrice).toFixed(decimalPlaces)}`; const rawUsdPrice = ethUnitAmount.mul(ethUsdPrice);
const roundedUsdPrice = rawUsdPrice.toFixed(decimalPlaces);
if (roundedUsdPrice === '0.00' && rawUsdPrice.gt(BIG_NUMBER_ZERO)) {
return '<$0.01';
} else {
return `$${roundedUsdPrice}`;
}
}, },
ethAddress: (address: string): string => { ethAddress: (address: string): string => {
return `0x${address.slice(2, 7)}${address.slice(-5)}`; return `0x${address.slice(2, 7)}${address.slice(-5)}`;

View File

@ -41,6 +41,18 @@ describe('format', () => {
it('converts BigNumber(5.3014059295032) to the string `5.301 ETH`', () => { it('converts BigNumber(5.3014059295032) to the string `5.301 ETH`', () => {
expect(format.ethUnitAmount(BIG_NUMBER_IRRATIONAL)).toBe('5.301 ETH'); expect(format.ethUnitAmount(BIG_NUMBER_IRRATIONAL)).toBe('5.301 ETH');
}); });
it('shows 1 significant digit when rounded amount would be 0', () => {
expect(format.ethUnitAmount(new BigNumber(0.00003))).toBe('0.00003 ETH');
expect(format.ethUnitAmount(new BigNumber(0.000034))).toBe('0.00003 ETH');
expect(format.ethUnitAmount(new BigNumber(0.000035))).toBe('0.00004 ETH');
});
it('shows < 0.00001 when hits threshold', () => {
expect(format.ethUnitAmount(new BigNumber(0.000011))).toBe('0.00001 ETH');
expect(format.ethUnitAmount(new BigNumber(0.00001))).toBe('0.00001 ETH');
expect(format.ethUnitAmount(new BigNumber(0.000009))).toBe('< 0.00001 ETH');
expect(format.ethUnitAmount(new BigNumber(0.0000000009))).toBe('< 0.00001 ETH');
expect(format.ethUnitAmount(new BigNumber(0))).toBe('0 ETH');
});
it('returns defaultText param when ethUnitAmount is not defined', () => { it('returns defaultText param when ethUnitAmount is not defined', () => {
const defaultText = 'defaultText'; const defaultText = 'defaultText';
expect(format.ethUnitAmount(undefined, 4, defaultText)).toBe(defaultText); expect(format.ethUnitAmount(undefined, 4, defaultText)).toBe(defaultText);
@ -86,6 +98,12 @@ describe('format', () => {
it('correctly formats 5.3014059295032 ETH to usd according to some price', () => { it('correctly formats 5.3014059295032 ETH to usd according to some price', () => {
expect(format.ethUnitAmountInUsd(BIG_NUMBER_IRRATIONAL, BIG_NUMBER_FAKE_ETH_USD_PRICE)).toBe('$13.43'); expect(format.ethUnitAmountInUsd(BIG_NUMBER_IRRATIONAL, BIG_NUMBER_FAKE_ETH_USD_PRICE)).toBe('$13.43');
}); });
it('correctly formats amount that is less than 1 cent', () => {
expect(format.ethUnitAmountInUsd(new BigNumber(0.000001), BIG_NUMBER_FAKE_ETH_USD_PRICE)).toBe('<$0.01');
});
it('correctly formats exactly 1 cent', () => {
expect(format.ethUnitAmountInUsd(new BigNumber(0.0039), BIG_NUMBER_FAKE_ETH_USD_PRICE)).toBe('$0.01');
});
it('returns defaultText param when ethUnitAmountInUsd or ethUsdPrice is not defined', () => { it('returns defaultText param when ethUnitAmountInUsd or ethUsdPrice is not defined', () => {
const defaultText = 'defaultText'; const defaultText = 'defaultText';
expect(format.ethUnitAmountInUsd(undefined, undefined, 2, defaultText)).toBe(defaultText); expect(format.ethUnitAmountInUsd(undefined, undefined, 2, defaultText)).toBe(defaultText);