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 => {
const ethAmount = format.ethBaseUnitAmount(
this.props.totalEthBaseUnitAmount,
4,
<AmountPlaceholder isPulsating={false} color={PLACEHOLDER_COLOR} />,
);
const fontSize = _.isString(ethAmount) && ethAmount.length >= 13 ? '14px' : '16px';
return (
<Text
fontSize="16px"
fontSize={fontSize}
textAlign="right"
width="100%"
fontColor={ColorOption.white}
fontWeight={500}
noWrap={true}
>
{format.ethBaseUnitAmount(
this.props.totalEthBaseUnitAmount,
4,
<AmountPlaceholder isPulsating={false} color={PLACEHOLDER_COLOR} />,
)}
{ethAmount}
</Text>
);
};

View File

@ -4,124 +4,227 @@ import * as _ from 'lodash';
import * as React from 'react';
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 { BaseCurrency } from '../types';
import { format } from '../util/format';
import { AmountPlaceholder } from './amount_placeholder';
import { SectionHeader } from './section_header';
import { Container } from './ui/container';
import { Flex } from './ui/flex';
import { Text } from './ui/text';
import { Text, TextProps } from './ui/text';
export interface OrderDetailsProps {
buyQuoteInfo?: BuyQuoteInfo;
selectedAssetUnitAmount?: BigNumber;
ethUsdPrice?: BigNumber;
isLoading: boolean;
assetName?: string;
baseCurrency: BaseCurrency;
onBaseCurrencySwitchEth: () => void;
onBaseCurrencySwitchUsd: () => void;
}
export class OrderDetails extends React.Component<OrderDetailsProps> {
public render(): React.ReactNode {
const { buyQuoteInfo, ethUsdPrice, selectedAssetUnitAmount } = this.props;
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;
const shouldShowUsdError = this.props.baseCurrency === BaseCurrency.USD && this._hadErrorFetchingUsdPrice();
return (
<Container width="100%" flexGrow={1} padding="20px 20px 0px 20px">
<Container marginBottom="10px">
<Text
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 marginBottom="10px">{this._renderHeader()}</Container>
{shouldShowUsdError ? this._renderErrorFetchingUsdPrice() : this._renderRows()}
</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 {
rowLabel: string;
ethAmount?: BigNumber;
isEthAmountInBaseUnits?: boolean;
ethUsdPrice?: BigNumber;
shouldEmphasize?: boolean;
isLoading: boolean;
export interface OrderDetailsRowProps {
labelText: React.ReactNode;
isLabelBold?: boolean;
isPrimaryValueBold?: boolean;
primaryValue: React.ReactNode;
secondaryValue?: React.ReactNode;
}
export class EthAmountRow extends React.Component<EthAmountRowProps> {
public static defaultProps = {
shouldEmphasize: false,
isEthAmountInBaseUnits: true,
};
export class OrderDetailsRow extends React.Component<OrderDetailsRowProps, {}> {
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 (
<Container padding="10px 0px" borderTop="1px dashed" borderColor={ColorOption.feintGrey}>
<Flex justify="space-between">
<Text fontWeight={fontWeight} fontColor={ColorOption.grey}>
{rowLabel}
<Text fontWeight={this.props.isLabelBold ? 700 : 400} fontColor={ColorOption.grey}>
{this.props.labelText}
</Text>
<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>
<Container>{this._renderValues()}</Container>
</Flex>
</Container>
);
}
private _renderUsdSection(): React.ReactNode {
const usdFormatter = this.props.isEthAmountInBaseUnits
? format.ethBaseUnitAmountInUsd
: format.ethUnitAmountInUsd;
const shouldHideUsdPriceSection = _.isUndefined(this.props.ethUsdPrice) || _.isUndefined(this.props.ethAmount);
return shouldHideUsdPriceSection ? null : (
private _renderValues(): React.ReactNode {
const secondaryValueNode: React.ReactNode = this.props.secondaryValue && (
<Container marginRight="3px" display="inline-block">
<Text fontColor={ColorOption.lightGrey}>
({usdFormatter(this.props.ethAmount, this.props.ethUsdPrice)})
</Text>
<Text fontColor={ColorOption.lightGrey}>({this.props.secondaryValue})</Text>
</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 { MetaMaskLogo } from './meta_mask_logo';
import { PaymentMethodDropdown } from './payment_method_dropdown';
import { SectionHeader } from './section_header';
import { Circle } from './ui/circle';
import { Container } from './ui/container';
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 marginBottom="12px">
<Flex justify="space-between">
<Text
letterSpacing="1px"
fontColor={ColorOption.primaryColor}
fontWeight={600}
textTransform="uppercase"
fontSize="14px"
>
{this._renderTitleText()}
</Text>
<SectionHeader>{this._renderTitleText()}</SectionHeader>
{this._renderTitleLabel()}
</Flex>
</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,
state.selectedAsset,
this.props.affiliateInfo,
state.baseCurrency,
),
);
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 NODE_ENV = process.env.NODE_ENV;
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 BUY_QUOTE_UPDATE_INTERVAL_TIME_MS = ONE_SECOND_MS * 15;
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 React from 'react';
import { connect } from 'react-redux';
import { Dispatch } from 'redux';
import { oc } from 'ts-optchain';
import { Action, actions } from '../redux/actions';
import { State } from '../redux/reducer';
import { OrderDetails } from '../components/order_details';
import { AsyncProcessState } from '../types';
import { OrderDetails, OrderDetailsProps } from '../components/order_details';
import { AsyncProcessState, BaseCurrency, Omit } from '../types';
import { assetUtils } from '../util/asset';
export interface LatestBuyQuoteOrderDetailsProps {}
interface ConnectedState {
buyQuoteInfo?: BuyQuoteInfo;
selectedAssetUnitAmount?: BigNumber;
ethUsdPrice?: BigNumber;
isLoading: boolean;
}
type DispatchProperties = 'onBaseCurrencySwitchEth' | 'onBaseCurrencySwitchUsd';
interface ConnectedState extends Omit<OrderDetailsProps, DispatchProperties> {}
const mapStateToProps = (state: State, _ownProps: LatestBuyQuoteOrderDetailsProps): ConnectedState => ({
// use the worst case quote info
buyQuoteInfo: oc(state).latestBuyQuote.worstCaseQuoteInfo(),
selectedAssetUnitAmount: state.selectedAssetUnitAmount,
ethUsdPrice: state.ethUsdPrice,
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(
mapStateToProps,
mapDispatchToProps,
)(OrderDetails);

View File

@ -2,7 +2,7 @@ import { BuyQuote } from '@0x/asset-buyer';
import { BigNumber } from '@0x/utils';
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> {
type: T;
@ -43,6 +43,7 @@ export enum ActionTypes {
RESET_AMOUNT = 'RESET_AMOUNT',
OPEN_STANDARD_SLIDING_PANEL = 'OPEN_STANDARD_SLIDING_PANEL',
CLOSE_STANDARD_SLIDING_PANEL = 'CLOSE_STANDARD_SLIDING_PANEL',
UPDATE_BASE_CURRENCY = 'UPDATE_BASE_CURRENCY',
}
export const actions = {
@ -72,4 +73,5 @@ export const actions = {
openStandardSlidingPanel: (content: StandardSlidingPanelContent) =>
createAction(ActionTypes.OPEN_STANDARD_SLIDING_PANEL, content),
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();
}
break;
case ActionTypes.UPDATE_BASE_CURRENCY:
analytics.trackBaseCurrencyChanged(curState.baseCurrency);
analytics.addEventProperties({ baseCurrency: curState.baseCurrency });
}
return nextAction;

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@ import { BigNumber } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import * as _ from 'lodash';
import { ETH_DECIMALS } from '../constants';
import { BIG_NUMBER_ZERO, ETH_DECIMALS } from '../constants';
export const format = {
ethBaseUnitAmount: (
@ -20,24 +20,38 @@ export const format = {
ethUnitAmount?: BigNumber,
decimalPlaces: number = 4,
defaultText: React.ReactNode = '0 ETH',
minUnitAmountToDisplay: BigNumber = new BigNumber('0.00001'),
): React.ReactNode => {
if (_.isUndefined(ethUnitAmount)) {
return defaultText;
}
const roundedAmount = ethUnitAmount.round(decimalPlaces).toDigits(decimalPlaces);
return `${roundedAmount} ETH`;
let roundedAmount = ethUnitAmount.round(decimalPlaces).toDigits(decimalPlaces);
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: (
ethBaseUnitAmount?: BigNumber,
ethUsdPrice?: BigNumber,
decimalPlaces: number = 2,
defaultText: React.ReactNode = '$0.00',
minUnitAmountToDisplay: BigNumber = new BigNumber('0.00001'),
): React.ReactNode => {
if (_.isUndefined(ethBaseUnitAmount) || _.isUndefined(ethUsdPrice)) {
return defaultText;
}
const ethUnitAmount = Web3Wrapper.toUnitAmount(ethBaseUnitAmount, ETH_DECIMALS);
return format.ethUnitAmountInUsd(ethUnitAmount, ethUsdPrice, decimalPlaces);
return format.ethUnitAmountInUsd(ethUnitAmount, ethUsdPrice, decimalPlaces, minUnitAmountToDisplay);
},
ethUnitAmountInUsd: (
ethUnitAmount?: BigNumber,
@ -48,7 +62,13 @@ export const format = {
if (_.isUndefined(ethUnitAmount) || _.isUndefined(ethUsdPrice)) {
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 => {
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`', () => {
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', () => {
const defaultText = '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', () => {
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', () => {
const defaultText = 'defaultText';
expect(format.ethUnitAmountInUsd(undefined, undefined, 2, defaultText)).toBe(defaultText);