aboutsummaryrefslogtreecommitdiffstats
path: root/packages/instant
diff options
context:
space:
mode:
authorSteve Klebanoff <steve.klebanoff@gmail.com>2018-12-08 03:14:55 +0800
committerSteve Klebanoff <steve.klebanoff@gmail.com>2018-12-12 06:37:45 +0800
commit5cbe04acab982ead39f7794547de0f045952a1b7 (patch)
tree5847cfb56df0c357997d5b1dfeb7811394a5da98 /packages/instant
parentd37680610b772d7bb585203047bef0af0439df0a (diff)
downloaddexon-0x-contracts-5cbe04acab982ead39f7794547de0f045952a1b7.tar.gz
dexon-0x-contracts-5cbe04acab982ead39f7794547de0f045952a1b7.tar.zst
dexon-0x-contracts-5cbe04acab982ead39f7794547de0f045952a1b7.zip
feat(instant): ETH/USD toggle
Diffstat (limited to 'packages/instant')
-rw-r--r--packages/instant/src/components/order_details.tsx196
-rw-r--r--packages/instant/src/containers/latest_buy_quote_order_details.ts33
-rw-r--r--packages/instant/src/redux/actions.ts4
-rw-r--r--packages/instant/src/redux/reducer.ts8
-rw-r--r--packages/instant/src/types.ts5
-rw-r--r--packages/instant/src/util/buy_quote.ts71
6 files changed, 269 insertions, 48 deletions
diff --git a/packages/instant/src/components/order_details.tsx b/packages/instant/src/components/order_details.tsx
index a8e0e2513..85761a5b9 100644
--- a/packages/instant/src/components/order_details.tsx
+++ b/packages/instant/src/components/order_details.tsx
@@ -6,63 +6,92 @@ import { oc } from 'ts-optchain';
import { BIG_NUMBER_ZERO } from '../constants';
import { ColorOption } from '../style/theme';
+import { BaseCurrency } from '../types';
+import { buyQuoteUtil } from '../util/buy_quote';
import { format } from '../util/format';
import { AmountPlaceholder } from './amount_placeholder';
import { Container } from './ui/container';
import { Flex } from './ui/flex';
-import { Text } from './ui/text';
+import { Text, TextProps } from './ui/text';
+
+interface BaseCurrenySwitchProps {
+ currencyName: string;
+ onClick: () => void;
+ isSelected: boolean;
+}
+const BaseCurrencySelector: React.StatelessComponent<BaseCurrenySwitchProps> = props => {
+ const textStyle: TextProps = { onClick: props.onClick, fontSize: '12px' };
+ if (props.isSelected) {
+ textStyle.fontColor = ColorOption.primaryColor;
+ textStyle.fontWeight = 700;
+ }
+ return <Text {...textStyle}>{props.currencyName}</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 weiAmounts = buyQuoteUtil.getWeiAmounts(selectedAssetUnitAmount, buyQuoteInfo);
+
+ const displayAmounts =
+ this.props.baseCurrency === BaseCurrency.USD
+ ? buyQuoteUtil.displayAmountsUsd(weiAmounts, ethUsdPrice)
+ : buyQuoteUtil.displayAmountsEth(weiAmounts, ethUsdPrice);
+
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>
+ <Flex justify="space-between">
+ <Text
+ letterSpacing="1px"
+ fontColor={ColorOption.primaryColor}
+ fontWeight={600}
+ textTransform="uppercase"
+ fontSize="14px"
+ >
+ Order Details
+ </Text>
+
+ <Container>
+ <BaseCurrencySelector
+ onClick={this.props.onBaseCurrencySwitchEth}
+ currencyName="ETH"
+ isSelected={this.props.baseCurrency === BaseCurrency.ETH}
+ />
+ <Container marginLeft="3px" marginRight="3px" display="inline">
+ <Text fontSize="12px">/</Text>
+ </Container>
+ <BaseCurrencySelector
+ onClick={this.props.onBaseCurrencySwitchUsd}
+ currencyName="USD"
+ isSelected={this.props.baseCurrency === BaseCurrency.USD}
+ />
+ </Container>
+ </Flex>
</Container>
- <EthAmountRow
- rowLabel="Token Price"
- ethAmount={pricePerTokenEth}
- ethUsdPrice={ethUsdPrice}
- isLoading={this.props.isLoading}
- />
- <EthAmountRow
- rowLabel="Fee"
- ethAmount={feeEthBaseUnitAmount}
- ethUsdPrice={ethUsdPrice}
+ <TokenAmountRow
+ numTokens={selectedAssetUnitAmount}
+ assetName={this.props.assetName}
+ displayPricePerToken={displayAmounts.pricePerToken}
+ displayTotalPrice={displayAmounts.assetTotal}
isLoading={this.props.isLoading}
/>
- <EthAmountRow
- rowLabel="Total Cost"
- ethAmount={totalEthBaseUnitAmount}
- ethUsdPrice={ethUsdPrice}
- shouldEmphasize={true}
+ <OrderDetailsRow labelText="Fee" value={displayAmounts.feeTotal} isLoading={this.props.isLoading} />
+ <TotalCostRow
+ displaySecondaryTotalCost={displayAmounts.secondaryGrandTotal}
+ displayPrimaryTotalCost={displayAmounts.primaryGrandTotal}
isLoading={this.props.isLoading}
/>
</Container>
@@ -79,6 +108,103 @@ export interface EthAmountRowProps {
isLoading: boolean;
}
+export interface OrderDetailsRowProps {
+ labelText: string;
+ isLabelBold?: boolean;
+ isLoading: boolean;
+ value?: React.ReactNode;
+}
+export class OrderDetailsRow extends React.Component<OrderDetailsRowProps, {}> {
+ public render(): React.ReactNode {
+ const { labelText, value, isLabelBold, isLoading } = this.props;
+ return (
+ <Container padding="10px 0px" borderTop="1px dashed" borderColor={ColorOption.feintGrey}>
+ <Flex justify="space-between">
+ <Text fontWeight={isLabelBold ? 700 : 400} fontColor={ColorOption.grey}>
+ {labelText}
+ </Text>
+ <Container>
+ {value || (
+ <Container opacity={0.5}>
+ <AmountPlaceholder color={ColorOption.lightGrey} isPulsating={isLoading} />
+ </Container>
+ )}
+ </Container>
+ </Flex>
+ </Container>
+ );
+ }
+}
+export interface TotalCostRowProps {
+ displayPrimaryTotalCost?: React.ReactNode;
+ displaySecondaryTotalCost?: React.ReactNode;
+ isLoading: boolean;
+}
+export class TotalCostRow extends React.Component<TotalCostRowProps, {}> {
+ public render(): React.ReactNode {
+ let value: React.ReactNode;
+ if (this.props.displayPrimaryTotalCost) {
+ const secondaryText = this.props.displaySecondaryTotalCost && (
+ <Container marginRight="3px" display="inline-block">
+ <Text fontColor={ColorOption.lightGrey}>({this.props.displaySecondaryTotalCost})</Text>
+ </Container>
+ );
+ value = (
+ <React.Fragment>
+ {secondaryText}
+ <Text fontWeight={700} fontColor={ColorOption.grey}>
+ {this.props.displayPrimaryTotalCost}
+ </Text>
+ </React.Fragment>
+ );
+ }
+
+ return (
+ <OrderDetailsRow isLoading={this.props.isLoading} isLabelBold={true} labelText="Total Cost" value={value} />
+ );
+ }
+}
+
+export interface TokenAmountRowProps {
+ assetName?: string;
+ displayPricePerToken?: React.ReactNode;
+ displayTotalPrice?: React.ReactNode;
+ isLoading: boolean;
+ numTokens?: BigNumber;
+}
+export class TokenAmountRow extends React.Component<TokenAmountRowProps> {
+ public static DEFAULT_TEXT: string = 'Token Price';
+ public render(): React.ReactNode {
+ return (
+ <OrderDetailsRow
+ isLoading={this.props.isLoading}
+ labelText={this._labelText()}
+ value={this.props.displayTotalPrice}
+ />
+ );
+ }
+ private _labelText(): string {
+ if (this.props.isLoading) {
+ return TokenAmountRow.DEFAULT_TEXT;
+ }
+ const { numTokens, displayPricePerToken, assetName } = this.props;
+ if (numTokens) {
+ let numTokensWithSymbol = numTokens.toString();
+
+ if (assetName) {
+ numTokensWithSymbol += ` ${assetName}`;
+ }
+
+ if (displayPricePerToken) {
+ numTokensWithSymbol += ` @ ${displayPricePerToken}`;
+ }
+ return numTokensWithSymbol;
+ }
+
+ return TokenAmountRow.DEFAULT_TEXT;
+ }
+}
+
export class EthAmountRow extends React.Component<EthAmountRowProps> {
public static defaultProps = {
shouldEmphasize: false,
diff --git a/packages/instant/src/containers/latest_buy_quote_order_details.ts b/packages/instant/src/containers/latest_buy_quote_order_details.ts
index 5dfe535e7..148735c47 100644
--- a/packages/instant/src/containers/latest_buy_quote_order_details.ts
+++ b/packages/instant/src/containers/latest_buy_quote_order_details.ts
@@ -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);
diff --git a/packages/instant/src/redux/actions.ts b/packages/instant/src/redux/actions.ts
index 77e3dec12..9d7a61fc7 100644
--- a/packages/instant/src/redux/actions.ts
+++ b/packages/instant/src/redux/actions.ts
@@ -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),
};
diff --git a/packages/instant/src/redux/reducer.ts b/packages/instant/src/redux/reducer.ts
index a9a407b7d..4e734041f 100644
--- a/packages/instant/src/redux/reducer.ts
+++ b/packages/instant/src/redux/reducer.ts
@@ -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.ETH,
};
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;
}
diff --git a/packages/instant/src/types.ts b/packages/instant/src/types.ts
index 1c7490e63..e7c920f36 100644
--- a/packages/instant/src/types.ts
+++ b/packages/instant/src/types.ts
@@ -26,6 +26,11 @@ export enum QuoteFetchOrigin {
Heartbeat = 'Heartbeat',
}
+export enum BaseCurrency {
+ USD = 'USD',
+ ETH = 'ETH',
+}
+
export interface SimulatedProgress {
startTimeUnix: number;
expectedEndTimeUnix: number;
diff --git a/packages/instant/src/util/buy_quote.ts b/packages/instant/src/util/buy_quote.ts
new file mode 100644
index 000000000..acd4d389c
--- /dev/null
+++ b/packages/instant/src/util/buy_quote.ts
@@ -0,0 +1,71 @@
+import { BuyQuoteInfo } from '@0x/asset-buyer';
+import { BigNumber } from '@0x/utils';
+import * as _ from 'lodash';
+import { oc } from 'ts-optchain';
+
+import { format } from '../util/format';
+
+import { BIG_NUMBER_ZERO } from '../constants';
+
+export interface DisplayAmounts {
+ pricePerToken: React.ReactNode;
+ assetTotal: React.ReactNode;
+ feeTotal: React.ReactNode;
+ primaryGrandTotal: React.ReactNode;
+ secondaryGrandTotal?: React.ReactNode;
+}
+
+export interface BuyQuoteWeiAmounts {
+ assetTotalInWei: BigNumber | undefined;
+ feeTotalInWei: BigNumber | undefined;
+ grandTotalInWei: BigNumber | undefined;
+ pricePerTokenInWei: BigNumber | undefined;
+}
+
+const ethDisplayFormat = (amountInWei?: BigNumber) => {
+ return format.ethBaseUnitAmount(amountInWei, 4, '');
+};
+const usdDisplayFormat = (amountInWei?: BigNumber, ethUsdPrice?: BigNumber) => {
+ return format.ethBaseUnitAmountInUsd(amountInWei, ethUsdPrice, 2, '');
+};
+
+export const buyQuoteUtil = {
+ getWeiAmounts: (
+ selectedAssetUnitAmount: BigNumber | undefined,
+ buyQuoteInfo: BuyQuoteInfo | undefined,
+ ): BuyQuoteWeiAmounts => {
+ const buyQuoteAccessor = oc(buyQuoteInfo);
+ const assetTotalInWei = buyQuoteAccessor.assetEthAmount();
+ const pricePerTokenInWei =
+ !_.isUndefined(assetTotalInWei) &&
+ !_.isUndefined(selectedAssetUnitAmount) &&
+ !selectedAssetUnitAmount.eq(BIG_NUMBER_ZERO)
+ ? assetTotalInWei.div(selectedAssetUnitAmount).ceil()
+ : undefined;
+
+ return {
+ assetTotalInWei,
+ feeTotalInWei: buyQuoteAccessor.feeEthAmount(),
+ grandTotalInWei: buyQuoteAccessor.totalEthAmount(),
+ pricePerTokenInWei,
+ };
+ },
+ displayAmountsEth: (weiAmounts: BuyQuoteWeiAmounts, ethUsdPrice?: BigNumber): DisplayAmounts => {
+ return {
+ pricePerToken: ethDisplayFormat(weiAmounts.pricePerTokenInWei),
+ assetTotal: ethDisplayFormat(weiAmounts.assetTotalInWei),
+ feeTotal: ethDisplayFormat(weiAmounts.feeTotalInWei),
+ primaryGrandTotal: ethDisplayFormat(weiAmounts.grandTotalInWei),
+ secondaryGrandTotal: usdDisplayFormat(weiAmounts.grandTotalInWei, ethUsdPrice),
+ };
+ },
+ displayAmountsUsd: (weiAmounts: BuyQuoteWeiAmounts, ethUsdPrice?: BigNumber): DisplayAmounts => {
+ return {
+ pricePerToken: usdDisplayFormat(weiAmounts.pricePerTokenInWei, ethUsdPrice),
+ assetTotal: usdDisplayFormat(weiAmounts.assetTotalInWei, ethUsdPrice),
+ feeTotal: usdDisplayFormat(weiAmounts.feeTotalInWei, ethUsdPrice),
+ primaryGrandTotal: usdDisplayFormat(weiAmounts.grandTotalInWei, ethUsdPrice),
+ secondaryGrandTotal: ethDisplayFormat(weiAmounts.grandTotalInWei),
+ };
+ },
+};