Merge pull request #1424 from 0xProject/feature/instant/usd-eth-toggle
[instant] ETH/USD toggle
This commit is contained in:
commit
737d1dc54d
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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">
|
||||
<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
|
||||
letterSpacing="1px"
|
||||
onClick={this.props.onBaseCurrencySwitchEth}
|
||||
fontWeight={700}
|
||||
fontColor={ColorOption.primaryColor}
|
||||
fontWeight={600}
|
||||
textTransform="uppercase"
|
||||
fontSize="14px"
|
||||
>
|
||||
Order Details
|
||||
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>
|
||||
<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}
|
||||
/>
|
||||
{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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
20
packages/instant/src/components/section_header.tsx
Normal file
20
packages/instant/src/components/section_header.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -122,6 +122,7 @@ export class ZeroExInstantProvider extends React.Component<ZeroExInstantProvider
|
||||
window,
|
||||
state.selectedAsset,
|
||||
this.props.affiliateInfo,
|
||||
state.baseCurrency,
|
||||
),
|
||||
);
|
||||
analytics.trackInstantOpened();
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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),
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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) => {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -26,6 +26,11 @@ export enum QuoteFetchOrigin {
|
||||
Heartbeat = 'Heartbeat',
|
||||
}
|
||||
|
||||
export enum BaseCurrency {
|
||||
USD = 'USD',
|
||||
ETH = 'ETH',
|
||||
}
|
||||
|
||||
export interface SimulatedProgress {
|
||||
startTimeUnix: number;
|
||||
expectedEndTimeUnix: number;
|
||||
|
@ -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),
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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)}`;
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user