Merge pull request #1161 from 0xProject/feature/instant/order-details-loading-state

[instant] Order details loading state
This commit is contained in:
Steve Klebanoff
2018-10-19 12:28:52 -07:00
committed by GitHub
14 changed files with 206 additions and 135 deletions

View File

@@ -0,0 +1,32 @@
import * as React from 'react';
import { ColorOption } from '../style/theme';
import { Pulse } from './animations/pulse';
import { Text } from './ui';
interface PlainPlaceholder {
color: ColorOption;
}
const PlainPlaceholder: React.StatelessComponent<PlainPlaceholder> = props => (
<Text fontWeight="bold" fontColor={props.color}>
&mdash;
</Text>
);
export interface AmountPlaceholderProps {
color: ColorOption;
isPulsating: boolean;
}
export const AmountPlaceholder: React.StatelessComponent<AmountPlaceholderProps> = props => {
if (props.isPulsating) {
return (
<Pulse>
<PlainPlaceholder color={props.color} />
</Pulse>
);
} else {
return <PlainPlaceholder color={props.color} />;
}
};

View File

@@ -0,0 +1,15 @@
import { keyframes, styled } from '../../style/theme';
const pulsingKeyframes = keyframes`
0%, 100% {
opacity: 0.2;
}
50% {
opacity: 100;
}
`;
export const Pulse = styled.div`
animation-name: ${pulsingKeyframes}
animation-duration: 2s;
animation-iteration-count: infinite;
`;

View File

@@ -7,79 +7,81 @@ import { ColorOption } from '../style/theme';
import { AsyncProcessState } from '../types';
import { format } from '../util/format';
import { AmountPlaceholder } from './amount_placeholder';
import { Container, Flex, Text } from './ui';
export interface InstantHeadingProps {
selectedAssetAmount?: BigNumber;
totalEthBaseAmount?: BigNumber;
ethUsdPrice?: BigNumber;
quoteState: AsyncProcessState;
quoteRequestState: AsyncProcessState;
}
const Placeholder = () => (
<Text fontWeight="bold" fontColor={ColorOption.white}>
&mdash;
</Text>
);
const displaytotalEthBaseAmount = ({
selectedAssetAmount,
totalEthBaseAmount,
}: InstantHeadingProps): React.ReactNode => {
if (_.isUndefined(selectedAssetAmount)) {
return '0 ETH';
}
return format.ethBaseAmount(totalEthBaseAmount, 4, <Placeholder />);
};
const displayUsdAmount = ({
totalEthBaseAmount,
selectedAssetAmount,
ethUsdPrice,
}: InstantHeadingProps): React.ReactNode => {
if (_.isUndefined(selectedAssetAmount)) {
return '$0.00';
}
return format.ethBaseAmountInUsd(totalEthBaseAmount, ethUsdPrice, 2, <Placeholder />);
};
const loadingOrAmount = (quoteState: AsyncProcessState, amount: React.ReactNode): React.ReactNode => {
if (quoteState === AsyncProcessState.PENDING) {
const placeholderColor = ColorOption.white;
export class InstantHeading extends React.Component<InstantHeadingProps, {}> {
public render(): React.ReactNode {
return (
<Text fontWeight="bold" fontColor={ColorOption.white}>
&hellip;loading
</Text>
);
} else {
return amount;
}
};
export const InstantHeading: React.StatelessComponent<InstantHeadingProps> = props => (
<Container backgroundColor={ColorOption.primaryColor} padding="20px" width="100%" borderRadius="3px 3px 0px 0px">
<Container marginBottom="5px">
<Text
letterSpacing="1px"
fontColor={ColorOption.white}
opacity={0.7}
fontWeight={500}
textTransform="uppercase"
fontSize="12px"
<Container
backgroundColor={ColorOption.primaryColor}
padding="20px"
width="100%"
borderRadius="3px 3px 0px 0px"
>
I want to buy
</Text>
</Container>
<Flex direction="row" justify="space-between">
<SelectedAssetAmountInput fontSize="45px" />
<Flex direction="column" justify="space-between">
<Container marginBottom="5px">
<Text fontSize="16px" fontColor={ColorOption.white} fontWeight={500}>
{loadingOrAmount(props.quoteState, displaytotalEthBaseAmount(props))}
<Text
letterSpacing="1px"
fontColor={ColorOption.white}
opacity={0.7}
fontWeight={500}
textTransform="uppercase"
fontSize="12px"
>
I want to buy
</Text>
</Container>
<Text fontSize="16px" fontColor={ColorOption.white} opacity={0.7}>
{loadingOrAmount(props.quoteState, displayUsdAmount(props))}
</Text>
</Flex>
</Flex>
</Container>
);
<Flex direction="row" justify="space-between">
<SelectedAssetAmountInput fontSize="45px" />
<Flex direction="column" justify="space-between">
<Container marginBottom="5px">{this._placeholderOrAmount(this._ethAmount)}</Container>
<Container opacity={0.7}>{this._placeholderOrAmount(this._dollarAmount)}</Container>
</Flex>
</Flex>
</Container>
);
}
private _placeholderOrAmount(amountFunction: () => React.ReactNode): React.ReactNode {
if (this.props.quoteRequestState === AsyncProcessState.PENDING) {
return <AmountPlaceholder isPulsating={true} color={placeholderColor} />;
}
if (_.isUndefined(this.props.selectedAssetAmount)) {
return <AmountPlaceholder isPulsating={false} color={placeholderColor} />;
}
return amountFunction();
}
private readonly _ethAmount = (): React.ReactNode => {
return (
<Text fontSize="16px" fontColor={ColorOption.white} fontWeight={500}>
{format.ethBaseAmount(
this.props.totalEthBaseAmount,
4,
<AmountPlaceholder isPulsating={false} color={placeholderColor} />,
)}
</Text>
);
};
private readonly _dollarAmount = (): React.ReactNode => {
return (
<Text fontSize="16px" fontColor={ColorOption.white}>
{format.ethBaseAmountInUsd(
this.props.totalEthBaseAmount,
this.props.ethUsdPrice,
2,
<AmountPlaceholder isPulsating={false} color={ColorOption.white} />,
)}
</Text>
);
};
}

View File

@@ -7,13 +7,14 @@ import { oc } from 'ts-optchain';
import { ColorOption } from '../style/theme';
import { format } from '../util/format';
import { AmountPlaceholder } from './amount_placeholder';
import { Container, Flex, Text } from './ui';
export interface OrderDetailsProps {
buyQuoteInfo?: BuyQuoteInfo;
ethUsdPrice?: BigNumber;
isLoading: boolean;
}
export class OrderDetails extends React.Component<OrderDetailsProps> {
public render(): React.ReactNode {
const { buyQuoteInfo, ethUsdPrice } = this.props;
@@ -39,13 +40,20 @@ export class OrderDetails extends React.Component<OrderDetailsProps> {
ethAmount={ethAssetPrice}
ethUsdPrice={ethUsdPrice}
isEthAmountInBaseUnits={false}
isLoading={this.props.isLoading}
/>
<EthAmountRow
rowLabel="Fee"
ethAmount={ethTokenFee}
ethUsdPrice={ethUsdPrice}
isLoading={this.props.isLoading}
/>
<EthAmountRow rowLabel="Fee" ethAmount={ethTokenFee} ethUsdPrice={ethUsdPrice} />
<EthAmountRow
rowLabel="Total Cost"
ethAmount={totalEthAmount}
ethUsdPrice={ethUsdPrice}
shouldEmphasize={true}
isLoading={this.props.isLoading}
/>
</Container>
);
@@ -58,43 +66,50 @@ export interface EthAmountRowProps {
isEthAmountInBaseUnits?: boolean;
ethUsdPrice?: BigNumber;
shouldEmphasize?: boolean;
isLoading: boolean;
}
export const EthAmountRow: React.StatelessComponent<EthAmountRowProps> = ({
rowLabel,
ethAmount,
isEthAmountInBaseUnits,
ethUsdPrice,
shouldEmphasize,
}) => {
const fontWeight = shouldEmphasize ? 700 : 400;
const usdFormatter = isEthAmountInBaseUnits ? format.ethBaseAmountInUsd : format.ethUnitAmountInUsd;
const ethFormatter = isEthAmountInBaseUnits ? format.ethBaseAmount : format.ethUnitAmount;
const usdPriceSection = _.isUndefined(ethUsdPrice) ? null : (
<Container marginRight="3px" display="inline-block">
<Text fontColor={ColorOption.lightGrey}>({usdFormatter(ethAmount, ethUsdPrice)})</Text>
</Container>
);
return (
<Container padding="10px 0px" borderTop="1px dashed" borderColor={ColorOption.feintGrey}>
<Flex justify="space-between">
<Text fontWeight={fontWeight} fontColor={ColorOption.grey}>
{rowLabel}
</Text>
<Container>
{usdPriceSection}
export class EthAmountRow extends React.Component<EthAmountRowProps> {
public static defaultProps = {
shouldEmphasize: false,
isEthAmountInBaseUnits: true,
};
public render(): React.ReactNode {
const { rowLabel, ethAmount, isEthAmountInBaseUnits, shouldEmphasize, isLoading } = this.props;
const fontWeight = shouldEmphasize ? 700 : 400;
const ethFormatter = isEthAmountInBaseUnits ? format.ethBaseAmount : format.ethUnitAmount;
return (
<Container padding="10px 0px" borderTop="1px dashed" borderColor={ColorOption.feintGrey}>
<Flex justify="space-between">
<Text fontWeight={fontWeight} fontColor={ColorOption.grey}>
{ethFormatter(ethAmount)}
{rowLabel}
</Text>
</Container>
</Flex>
</Container>
);
};
EthAmountRow.defaultProps = {
shouldEmphasize: false,
isEthAmountInBaseUnits: true,
};
EthAmountRow.displayName = 'EthAmountRow';
<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>
</Container>
);
}
private _renderUsdSection(): React.ReactNode {
const usdFormatter = this.props.isEthAmountInBaseUnits ? format.ethBaseAmountInUsd : format.ethUnitAmountInUsd;
const shouldHideUsdPriceSection = _.isUndefined(this.props.ethUsdPrice) || _.isUndefined(this.props.ethAmount);
return shouldHideUsdPriceSection ? null : (
<Container marginRight="3px" display="inline-block">
<Text fontColor={ColorOption.lightGrey}>
({usdFormatter(this.props.ethAmount, this.props.ethUsdPrice)})
</Text>
</Container>
);
}
}

View File

@@ -27,6 +27,7 @@ export interface ContainerProps {
backgroundColor?: ColorOption;
hasBoxShadow?: boolean;
zIndex?: number;
opacity?: number;
}
const PlainContainer: React.StatelessComponent<ContainerProps> = ({ children, className }) => (
@@ -54,6 +55,7 @@ export const Container = styled(PlainContainer)`
${props => cssRuleIfExists(props, 'border-top')}
${props => cssRuleIfExists(props, 'border-bottom')}
${props => cssRuleIfExists(props, 'z-index')}
${props => cssRuleIfExists(props, 'opacity')}
${props => (props.hasBoxShadow ? `box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.1)` : '')};
background-color: ${props => (props.backgroundColor ? props.theme[props.backgroundColor] : 'none')};
border-color: ${props => (props.borderColor ? props.theme[props.borderColor] : 'none')};

View File

@@ -8,18 +8,21 @@ import { oc } from 'ts-optchain';
import { State } from '../redux/reducer';
import { OrderDetails } from '../components/order_details';
import { AsyncProcessState } from '../types';
export interface LatestBuyQuoteOrderDetailsProps {}
interface ConnectedState {
buyQuoteInfo?: BuyQuoteInfo;
ethUsdPrice?: BigNumber;
isLoading: boolean;
}
const mapStateToProps = (state: State, _ownProps: LatestBuyQuoteOrderDetailsProps): ConnectedState => ({
// use the worst case quote info
buyQuoteInfo: oc(state).latestBuyQuote.worstCaseQuoteInfo(),
ethUsdPrice: state.ethUsdPrice,
isLoading: state.quoteRequestState === AsyncProcessState.PENDING,
});
export const LatestBuyQuoteOrderDetails: React.ComponentClass<LatestBuyQuoteOrderDetailsProps> = connect(

View File

@@ -3,7 +3,8 @@ import * as React from 'react';
import { connect } from 'react-redux';
import { SlidingError } from '../components/sliding_error';
import { LatestErrorDisplay, State } from '../redux/reducer';
import { State } from '../redux/reducer';
import { DisplayStatus } from '../types';
import { errorUtil } from '../util/error';
export interface LatestErrorComponentProps {
@@ -29,7 +30,7 @@ export interface LatestErrorProps {}
const mapStateToProps = (state: State, _ownProps: LatestErrorProps): ConnectedState => ({
assetData: state.selectedAssetData,
latestError: state.latestError,
slidingDirection: state.latestErrorDisplay === LatestErrorDisplay.Present ? 'up' : 'down',
slidingDirection: state.latestErrorDisplay === DisplayStatus.Present ? 'up' : 'down',
});
export const LatestError = connect(mapStateToProps)(LatestErrorComponent);

View File

@@ -44,13 +44,13 @@ const updateBuyQuoteAsync = async (
const baseUnitValue = Web3Wrapper.toBaseUnitAmount(assetAmount, zrxDecimals);
// mark quote as pending
dispatch(actions.updateBuyQuoteStatePending());
dispatch(actions.setQuoteRequestStatePending());
let newBuyQuote: BuyQuote | undefined;
try {
newBuyQuote = await assetBuyer.getBuyQuoteAsync(assetData, baseUnitValue);
} catch (error) {
dispatch(actions.updateBuyQuoteStateFailure());
dispatch(actions.setQuoteRequestStateFailure());
errorUtil.errorFlasher.flashNewError(dispatch, error);
return;
}
@@ -72,11 +72,11 @@ const mapDispatchToProps = (
// invalidate the last buy quote.
dispatch(actions.updateLatestBuyQuote(undefined));
// reset our buy state
dispatch(actions.updatebuyOrderState(AsyncProcessState.NONE));
dispatch(actions.updateBuyOrderState(AsyncProcessState.NONE));
if (!_.isUndefined(value) && !_.isUndefined(assetData)) {
// even if it's debounced, give them the illusion it's loading
dispatch(actions.updateBuyQuoteStatePending());
dispatch(actions.setQuoteRequestStatePending());
// tslint:disable-next-line:no-floating-promises
debouncedUpdateBuyQuoteAsync(dispatch, assetData, value);
}

View File

@@ -44,9 +44,9 @@ const mapStateToProps = (state: State, _ownProps: SelectedAssetBuyButtonProps):
});
const mapDispatchToProps = (dispatch: Dispatch<Action>, ownProps: SelectedAssetBuyButtonProps): ConnectedDispatch => ({
onClick: buyQuote => dispatch(actions.updatebuyOrderState(AsyncProcessState.PENDING)),
onBuySuccess: buyQuote => dispatch(actions.updatebuyOrderState(AsyncProcessState.SUCCESS)),
onBuyFailure: buyQuote => dispatch(actions.updatebuyOrderState(AsyncProcessState.FAILURE)),
onClick: buyQuote => dispatch(actions.updateBuyOrderState(AsyncProcessState.PENDING)),
onBuySuccess: buyQuote => dispatch(actions.updateBuyOrderState(AsyncProcessState.SUCCESS)),
onBuyFailure: buyQuote => dispatch(actions.updateBuyOrderState(AsyncProcessState.FAILURE)),
});
export const SelectedAssetBuyButton: React.ComponentClass<SelectedAssetBuyButtonProps> = connect(

View File

@@ -15,14 +15,14 @@ interface ConnectedState {
selectedAssetAmount?: BigNumber;
totalEthBaseAmount?: BigNumber;
ethUsdPrice?: BigNumber;
quoteState: AsyncProcessState;
quoteRequestState: AsyncProcessState;
}
const mapStateToProps = (state: State, _ownProps: InstantHeadingProps): ConnectedState => ({
selectedAssetAmount: state.selectedAssetAmount,
totalEthBaseAmount: oc(state).latestBuyQuote.worstCaseQuoteInfo.totalEthAmount(),
ethUsdPrice: state.ethUsdPrice,
quoteState: state.quoteState,
quoteRequestState: state.quoteRequestState,
});
export const SelectedAssetInstantHeading: React.ComponentClass<InstantHeadingProps> = connect(mapStateToProps)(

View File

@@ -25,8 +25,8 @@ export enum ActionTypes {
UPDATE_SELECTED_ASSET_AMOUNT = 'UPDATE_SELECTED_ASSET_AMOUNT',
UPDATE_SELECTED_ASSET_BUY_STATE = 'UPDATE_SELECTED_ASSET_BUY_STATE',
UPDATE_LATEST_BUY_QUOTE = 'UPDATE_LATEST_BUY_QUOTE',
UPDATE_BUY_QUOTE_STATE_PENDING = 'UPDATE_BUY_QUOTE_STATE_PENDING',
UPDATE_BUY_QUOTE_STATE_FAILURE = 'UPDATE_BUY_QUOTE_STATE_FAILURE',
SET_QUOTE_REQUEST_STATE_PENDING = 'SET_QUOTE_REQUEST_STATE_PENDING',
SET_QUOTE_REQUEST_STATE_FAILURE = 'SET_QUOTE_REQUEST_STATE_FAILURE',
SET_ERROR = 'SET_ERROR',
HIDE_ERROR = 'HIDE_ERROR',
CLEAR_ERROR = 'CLEAR_ERROR',
@@ -35,11 +35,11 @@ export enum ActionTypes {
export const actions = {
updateEthUsdPrice: (price?: BigNumber) => createAction(ActionTypes.UPDATE_ETH_USD_PRICE, price),
updateSelectedAssetAmount: (amount?: BigNumber) => createAction(ActionTypes.UPDATE_SELECTED_ASSET_AMOUNT, amount),
updatebuyOrderState: (buyState: AsyncProcessState) =>
updateBuyOrderState: (buyState: AsyncProcessState) =>
createAction(ActionTypes.UPDATE_SELECTED_ASSET_BUY_STATE, buyState),
updateLatestBuyQuote: (buyQuote?: BuyQuote) => createAction(ActionTypes.UPDATE_LATEST_BUY_QUOTE, buyQuote),
updateBuyQuoteStatePending: () => createAction(ActionTypes.UPDATE_BUY_QUOTE_STATE_PENDING),
updateBuyQuoteStateFailure: () => createAction(ActionTypes.UPDATE_BUY_QUOTE_STATE_FAILURE),
setQuoteRequestStatePending: () => createAction(ActionTypes.SET_QUOTE_REQUEST_STATE_PENDING),
setQuoteRequestStateFailure: () => createAction(ActionTypes.SET_QUOTE_REQUEST_STATE_FAILURE),
setError: (error?: any) => createAction(ActionTypes.SET_ERROR, error),
hideError: () => createAction(ActionTypes.HIDE_ERROR),
clearError: () => createAction(ActionTypes.CLEAR_ERROR),

View File

@@ -3,23 +3,19 @@ import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { zrxAssetData } from '../constants';
import { AsyncProcessState } from '../types';
import { AsyncProcessState, DisplayStatus } from '../types';
import { Action, ActionTypes } from './actions';
export enum LatestErrorDisplay {
Present,
Hidden,
}
export interface State {
selectedAssetData?: string;
selectedAssetAmount?: BigNumber;
buyOrderState: AsyncProcessState;
ethUsdPrice?: BigNumber;
latestBuyQuote?: BuyQuote;
quoteState: AsyncProcessState;
quoteRequestState: AsyncProcessState;
latestError?: any;
latestErrorDisplay: LatestErrorDisplay;
latestErrorDisplay: DisplayStatus;
}
export const INITIAL_STATE: State = {
@@ -30,8 +26,8 @@ export const INITIAL_STATE: State = {
ethUsdPrice: undefined,
latestBuyQuote: undefined,
latestError: undefined,
latestErrorDisplay: LatestErrorDisplay.Hidden,
quoteState: AsyncProcessState.NONE,
latestErrorDisplay: DisplayStatus.Hidden,
quoteRequestState: AsyncProcessState.NONE,
};
export const reducer = (state: State = INITIAL_STATE, action: Action): State => {
@@ -50,19 +46,19 @@ export const reducer = (state: State = INITIAL_STATE, action: Action): State =>
return {
...state,
latestBuyQuote: action.data,
quoteState: AsyncProcessState.SUCCESS,
quoteRequestState: AsyncProcessState.SUCCESS,
};
case ActionTypes.UPDATE_BUY_QUOTE_STATE_PENDING:
case ActionTypes.SET_QUOTE_REQUEST_STATE_PENDING:
return {
...state,
latestBuyQuote: undefined,
quoteState: AsyncProcessState.PENDING,
quoteRequestState: AsyncProcessState.PENDING,
};
case ActionTypes.UPDATE_BUY_QUOTE_STATE_FAILURE:
case ActionTypes.SET_QUOTE_REQUEST_STATE_FAILURE:
return {
...state,
latestBuyQuote: undefined,
quoteState: AsyncProcessState.FAILURE,
quoteRequestState: AsyncProcessState.FAILURE,
};
case ActionTypes.UPDATE_SELECTED_ASSET_BUY_STATE:
return {
@@ -73,18 +69,18 @@ export const reducer = (state: State = INITIAL_STATE, action: Action): State =>
return {
...state,
latestError: action.data,
latestErrorDisplay: LatestErrorDisplay.Present,
latestErrorDisplay: DisplayStatus.Present,
};
case ActionTypes.HIDE_ERROR:
return {
...state,
latestErrorDisplay: LatestErrorDisplay.Hidden,
latestErrorDisplay: DisplayStatus.Hidden,
};
case ActionTypes.CLEAR_ERROR:
return {
...state,
latestError: undefined,
latestErrorDisplay: LatestErrorDisplay.Hidden,
latestErrorDisplay: DisplayStatus.Hidden,
};
default:
return state;

View File

@@ -7,6 +7,10 @@ export enum AsyncProcessState {
SUCCESS = 'Success',
FAILURE = 'Failure',
}
export enum DisplayStatus {
Present,
Hidden,
}
export type FunctionType = (...args: any[]) => any;
export type ActionCreatorsMapObject = ObjectMap<FunctionType>;

View File

@@ -2,6 +2,7 @@
"extends": ["@0x/tslint-config"],
"rules": {
"custom-no-magic-numbers": false,
"semicolon": [true, "always", "ignore-bound-class-methods"]
"semicolon": [true, "always", "ignore-bound-class-methods"],
"max-classes-per-file": false
}
}