feat(instant): Handle AssetBuyer errors
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { keyframes, styled } from '../../style/theme';
|
||||
|
||||
const slideKeyframeGenerator = (fromY: string, toY: string) => keyframes`
|
||||
from {
|
||||
position: relative;
|
||||
top: ${fromY};
|
||||
}
|
||||
|
||||
to {
|
||||
position: relative;
|
||||
top: ${toY};
|
||||
}
|
||||
`;
|
||||
|
||||
export interface SlideAnimationProps {
|
||||
keyframes: string;
|
||||
animationType: string;
|
||||
animationDirection?: string;
|
||||
}
|
||||
|
||||
export const SlideAnimation =
|
||||
styled.div <
|
||||
SlideAnimationProps >
|
||||
`
|
||||
animation-name: ${props => props.keyframes};
|
||||
animation-duration: 0.3s;
|
||||
animation-timing-function: ${props => props.animationType};
|
||||
animation-delay: 0s;
|
||||
animation-iteration-count: 1;
|
||||
animation-fill-mode: ${props => props.animationDirection || 'none'};
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export interface SlideAnimationComponentProps {
|
||||
downY: string;
|
||||
}
|
||||
|
||||
export const SlideUpAnimation: React.StatelessComponent<SlideAnimationComponentProps> = props => (
|
||||
<SlideAnimation animationType="ease-in" keyframes={slideKeyframeGenerator(props.downY, '0px')}>
|
||||
{props.children}
|
||||
</SlideAnimation>
|
||||
);
|
||||
|
||||
export const SlideDownAnimation: React.StatelessComponent<SlideAnimationComponentProps> = props => (
|
||||
<SlideAnimation
|
||||
animationDirection="forwards"
|
||||
animationType="cubic-bezier(0.25, 0.1, 0.25, 1)"
|
||||
keyframes={slideKeyframeGenerator('0px', props.downY)}
|
||||
>
|
||||
{props.children}
|
||||
</SlideAnimation>
|
||||
);
|
||||
@@ -1,95 +0,0 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { keyframes, styled } from '../../style/theme';
|
||||
|
||||
const slideKeyframeGenerator = (fromY: string, toY: string) => keyframes`
|
||||
from {
|
||||
position: relative;
|
||||
top: ${fromY};
|
||||
}
|
||||
|
||||
to {
|
||||
position: relative;
|
||||
top: ${toY};
|
||||
}
|
||||
`;
|
||||
|
||||
export interface SlideAnimationProps {
|
||||
keyframes: string;
|
||||
animationType: string;
|
||||
animationDirection?: string;
|
||||
}
|
||||
export const SlideAnimation =
|
||||
styled.div <
|
||||
SlideAnimationProps >
|
||||
`
|
||||
animation-name: ${props => props.keyframes};
|
||||
animation-duration: 0.3s;
|
||||
animation-timing-function: ${props => props.animationType};
|
||||
animation-delay: 0s;
|
||||
animation-iteration-count: 1;
|
||||
animation-fill-mode: ${props => props.animationDirection || 'none'};
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export interface SlideAnimationComponentProps {
|
||||
downY: string;
|
||||
}
|
||||
|
||||
export const SlideUpAnimationComponent: React.StatelessComponent<SlideAnimationComponentProps> = props => (
|
||||
<SlideAnimation animationType="ease-in" keyframes={slideKeyframeGenerator(props.downY, '0px')}>
|
||||
{props.children}
|
||||
</SlideAnimation>
|
||||
);
|
||||
|
||||
export const SlideDownAnimationComponent: React.StatelessComponent<SlideAnimationComponentProps> = props => (
|
||||
<SlideAnimation
|
||||
animationDirection="forwards"
|
||||
animationType="cubic-bezier(0.25, 0.1, 0.25, 1)"
|
||||
keyframes={slideKeyframeGenerator('0px', props.downY)}
|
||||
>
|
||||
{props.children}
|
||||
</SlideAnimation>
|
||||
);
|
||||
|
||||
export interface SlideUpAndDownAnimationProps extends SlideAnimationComponentProps {
|
||||
delayMs: number;
|
||||
}
|
||||
|
||||
enum SlideState {
|
||||
Up = 'up',
|
||||
Down = 'down',
|
||||
}
|
||||
interface SlideUpAndDownState {
|
||||
slideState: SlideState;
|
||||
}
|
||||
|
||||
export class SlideUpAndDownAnimation extends React.Component<SlideUpAndDownAnimationProps, SlideUpAndDownState> {
|
||||
public state = {
|
||||
slideState: SlideState.Up,
|
||||
};
|
||||
|
||||
private _timeoutId?: number;
|
||||
public render(): React.ReactNode {
|
||||
return this._renderSlide();
|
||||
}
|
||||
public componentDidMount(): void {
|
||||
this._timeoutId = window.setTimeout(() => {
|
||||
this.setState({
|
||||
slideState: SlideState.Down,
|
||||
});
|
||||
}, this.props.delayMs);
|
||||
|
||||
return;
|
||||
}
|
||||
public componentWillUnmount(): void {
|
||||
if (this._timeoutId) {
|
||||
window.clearTimeout(this._timeoutId);
|
||||
}
|
||||
}
|
||||
private _renderSlide(): React.ReactNode {
|
||||
const SlideComponent = this.state.slideState === 'up' ? SlideUpAnimationComponent : SlideDownAnimationComponent;
|
||||
|
||||
return <SlideComponent downY={this.props.downY}>{this.props.children}</SlideComponent>;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,8 @@ import { BigNumber } from '@0xproject/utils';
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
|
||||
import { assetMetaData } from '../data/asset_meta_data';
|
||||
import { bestNameForAsset } from '../util/asset_data';
|
||||
|
||||
import { ColorOption } from '../style/theme';
|
||||
import { util } from '../util/util';
|
||||
|
||||
@@ -26,26 +27,12 @@ export class AssetAmountInput extends React.Component<AssetAmountInputProps> {
|
||||
<AmountInput {...rest} onChange={this._handleChange} />
|
||||
<Container display="inline-block" marginLeft="10px">
|
||||
<Text fontSize={rest.fontSize} fontColor={ColorOption.white} textTransform="uppercase">
|
||||
{this._getAssetSymbolLabel()}
|
||||
{bestNameForAsset(this.props.assetData, '???')}
|
||||
</Text>
|
||||
</Container>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
private readonly _getAssetSymbolLabel = (): string => {
|
||||
const unknownLabel = '???';
|
||||
if (_.isUndefined(this.props.assetData)) {
|
||||
return unknownLabel;
|
||||
}
|
||||
const metaData = assetMetaData[this.props.assetData];
|
||||
if (_.isUndefined(metaData)) {
|
||||
return unknownLabel;
|
||||
}
|
||||
if (metaData.assetProxyId === AssetProxyId.ERC20) {
|
||||
return metaData.symbol;
|
||||
}
|
||||
return unknownLabel;
|
||||
};
|
||||
private readonly _handleChange = (value?: BigNumber): void => {
|
||||
this.props.onChange(value, this.props.assetData);
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as React from 'react';
|
||||
|
||||
import { ColorOption } from '../style/theme';
|
||||
|
||||
import { SlideUpAndDownAnimation } from './animations/slide_up_and_down_animation';
|
||||
import { SlideDownAnimation, SlideUpAnimation } from './animations/slide_animations';
|
||||
|
||||
import { Container, Text } from './ui';
|
||||
|
||||
@@ -29,8 +29,16 @@ export const Error: React.StatelessComponent<ErrorProps> = props => (
|
||||
</Container>
|
||||
);
|
||||
|
||||
export const SlidingError: React.StatelessComponent<ErrorProps> = props => (
|
||||
<SlideUpAndDownAnimation downY="120px" delayMs={5000}>
|
||||
<Error icon={props.icon} message={props.message} />
|
||||
</SlideUpAndDownAnimation>
|
||||
);
|
||||
export type SlidingDirection = 'up' | 'down';
|
||||
export interface SlidingErrorProps extends ErrorProps {
|
||||
direction: SlidingDirection;
|
||||
}
|
||||
export const SlidingError: React.StatelessComponent<SlidingErrorProps> = props => {
|
||||
const AnimationComponent = props.direction === 'up' ? SlideUpAnimation : SlideDownAnimation;
|
||||
|
||||
return (
|
||||
<AnimationComponent downY="120px">
|
||||
<Error icon={props.icon} message={props.message} />
|
||||
</AnimationComponent>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { LatestBuyQuoteOrderDetails } from '../containers/latest_buy_quote_order_details';
|
||||
import { LatestError } from '../containers/latest_error';
|
||||
import { SelectedAssetBuyButton } from '../containers/selected_asset_buy_button';
|
||||
import { SelectedAssetInstantHeading } from '../containers/selected_asset_instant_heading';
|
||||
|
||||
@@ -16,6 +17,9 @@ export interface ZeroExInstantContainerProps {}
|
||||
|
||||
export const ZeroExInstantContainer: React.StatelessComponent<ZeroExInstantContainerProps> = props => (
|
||||
<Container width="350px">
|
||||
<Container zIndex={1} position="relative">
|
||||
<LatestError />
|
||||
</Container>
|
||||
<Container
|
||||
zIndex={2}
|
||||
position="relative"
|
||||
|
||||
36
packages/instant/src/containers/latest_error.tsx
Normal file
36
packages/instant/src/containers/latest_error.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { SlidingError } from '../components/sliding_error';
|
||||
import { State } from '../redux/reducer';
|
||||
import { errorDescription } from '../util/error_description';
|
||||
|
||||
export interface LatestErrorComponentProps {
|
||||
assetData?: string;
|
||||
latestError?: any;
|
||||
latestErrorDismissed?: boolean;
|
||||
}
|
||||
|
||||
export const LatestErrorComponent: React.StatelessComponent<LatestErrorComponentProps> = props => {
|
||||
if (!props.latestError) {
|
||||
return <div />;
|
||||
}
|
||||
const slidingDirection = props.latestErrorDismissed ? 'down' : 'up';
|
||||
const { icon, message } = errorDescription(props.latestError, props.assetData);
|
||||
return <SlidingError direction={slidingDirection} icon={icon} message={message} />;
|
||||
};
|
||||
|
||||
interface ConnectedState {
|
||||
assetData?: string;
|
||||
latestError?: any;
|
||||
latestErrorDismissed?: boolean;
|
||||
}
|
||||
export interface LatestErrorProps {}
|
||||
const mapStateToProps = (state: State, _ownProps: LatestErrorProps): ConnectedState => ({
|
||||
assetData: state.selectedAssetData,
|
||||
latestError: state.latestError,
|
||||
latestErrorDismissed: state.latestErrorDismissed,
|
||||
});
|
||||
|
||||
export const LatestError = connect(mapStateToProps)(LatestErrorComponent);
|
||||
@@ -1,3 +1,4 @@
|
||||
import { BuyQuote } from '@0xproject/asset-buyer';
|
||||
import { BigNumber } from '@0xproject/utils';
|
||||
import { Web3Wrapper } from '@0xproject/web3-wrapper';
|
||||
import * as _ from 'lodash';
|
||||
@@ -11,6 +12,7 @@ import { State } from '../redux/reducer';
|
||||
import { ColorOption } from '../style/theme';
|
||||
import { AsyncProcessState } from '../types';
|
||||
import { assetBuyer } from '../util/asset_buyer';
|
||||
import { errorFlasher } from '../util/error_flasher';
|
||||
|
||||
import { AssetAmountInput } from '../components/asset_amount_input';
|
||||
|
||||
@@ -43,7 +45,16 @@ const updateBuyQuoteAsync = async (
|
||||
}
|
||||
// get a new buy quote.
|
||||
const baseUnitValue = Web3Wrapper.toBaseUnitAmount(assetAmount, zrxDecimals);
|
||||
const newBuyQuote = await assetBuyer.getBuyQuoteAsync(assetData, baseUnitValue);
|
||||
|
||||
let newBuyQuote: BuyQuote | undefined;
|
||||
try {
|
||||
newBuyQuote = await assetBuyer.getBuyQuoteAsync(assetData, baseUnitValue);
|
||||
errorFlasher.clearError(dispatch);
|
||||
} catch (error) {
|
||||
errorFlasher.flashNewError(dispatch, error);
|
||||
return;
|
||||
}
|
||||
|
||||
// invalidate the last buy quote.
|
||||
dispatch(actions.updateLatestBuyQuote(newBuyQuote));
|
||||
};
|
||||
|
||||
@@ -25,6 +25,9 @@ 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',
|
||||
SET_ERROR = 'SET_ERROR',
|
||||
HIDE_ERROR = 'HIDE_ERROR',
|
||||
CLEAR_ERROR = 'CLEAR_ERROR',
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
@@ -33,4 +36,7 @@ export const actions = {
|
||||
updateSelectedAssetBuyState: (buyState: AsyncProcessState) =>
|
||||
createAction(ActionTypes.UPDATE_SELECTED_ASSET_BUY_STATE, buyState),
|
||||
updateLatestBuyQuote: (buyQuote?: BuyQuote) => createAction(ActionTypes.UPDATE_LATEST_BUY_QUOTE, buyQuote),
|
||||
setError: (error?: any) => createAction(ActionTypes.SET_ERROR, error),
|
||||
hideError: () => createAction(ActionTypes.HIDE_ERROR),
|
||||
clearError: () => createAction(ActionTypes.CLEAR_ERROR),
|
||||
};
|
||||
|
||||
@@ -7,13 +7,22 @@ import { AsyncProcessState } from '../types';
|
||||
|
||||
import { Action, ActionTypes } from './actions';
|
||||
|
||||
export interface State {
|
||||
interface BaseState {
|
||||
selectedAssetData?: string;
|
||||
selectedAssetAmount?: BigNumber;
|
||||
selectedAssetBuyState: AsyncProcessState;
|
||||
ethUsdPrice?: BigNumber;
|
||||
latestBuyQuote?: BuyQuote;
|
||||
}
|
||||
interface StateWithError extends BaseState {
|
||||
latestError: any;
|
||||
latestErrorDismissed: boolean;
|
||||
}
|
||||
interface StateWithoutError extends BaseState {
|
||||
latestError: undefined;
|
||||
latestErrorDismissed: undefined;
|
||||
}
|
||||
export type State = StateWithError | StateWithoutError;
|
||||
|
||||
export const INITIAL_STATE: State = {
|
||||
// TODO: Remove hardcoded zrxAssetData
|
||||
@@ -22,6 +31,8 @@ export const INITIAL_STATE: State = {
|
||||
selectedAssetBuyState: AsyncProcessState.NONE,
|
||||
ethUsdPrice: undefined,
|
||||
latestBuyQuote: undefined,
|
||||
latestError: undefined,
|
||||
latestErrorDismissed: undefined,
|
||||
};
|
||||
|
||||
export const reducer = (state: State = INITIAL_STATE, action: Action): State => {
|
||||
@@ -46,6 +57,23 @@ export const reducer = (state: State = INITIAL_STATE, action: Action): State =>
|
||||
...state,
|
||||
selectedAssetBuyState: action.data,
|
||||
};
|
||||
case ActionTypes.SET_ERROR:
|
||||
return {
|
||||
...state,
|
||||
latestError: action.data,
|
||||
latestErrorDismissed: false,
|
||||
};
|
||||
case ActionTypes.HIDE_ERROR:
|
||||
return {
|
||||
...state,
|
||||
latestErrorDismissed: true,
|
||||
};
|
||||
case ActionTypes.CLEAR_ERROR:
|
||||
return {
|
||||
...state,
|
||||
latestError: undefined,
|
||||
latestErrorDismissed: undefined,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
||||
@@ -3,4 +3,5 @@ import { createStore, Store as ReduxStore } from 'redux';
|
||||
|
||||
import { reducer, State } from './reducer';
|
||||
|
||||
export const store: ReduxStore<State> = createStore(reducer);
|
||||
const reduxDevTools = (window as any).__REDUX_DEVTOOLS_EXTENSION__;
|
||||
export const store: ReduxStore<State> = createStore(reducer, reduxDevTools && reduxDevTools());
|
||||
|
||||
18
packages/instant/src/util/asset_data.ts
Normal file
18
packages/instant/src/util/asset_data.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { AssetProxyId } from '@0xproject/types';
|
||||
|
||||
import { assetMetaData } from '../data/asset_meta_data';
|
||||
|
||||
// TODO: tests for this
|
||||
export const bestNameForAsset = (assetData: string | undefined, defaultString: string) => {
|
||||
if (assetData === undefined) {
|
||||
return defaultString;
|
||||
}
|
||||
const metaData = assetMetaData[assetData];
|
||||
if (metaData === undefined) {
|
||||
return defaultString;
|
||||
}
|
||||
if (metaData.assetProxyId === AssetProxyId.ERC20) {
|
||||
return metaData.symbol.toUpperCase();
|
||||
}
|
||||
return defaultString;
|
||||
};
|
||||
23
packages/instant/src/util/error_description.ts
Normal file
23
packages/instant/src/util/error_description.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { AssetBuyerError } from '@0xproject/asset-buyer';
|
||||
|
||||
import { bestNameForAsset } from '../util/asset_data';
|
||||
|
||||
const humanReadableMessageForError = (error: Error, assetData?: string): string | undefined => {
|
||||
if (error.message === AssetBuyerError.InsufficientAssetLiquidity) {
|
||||
const assetName = bestNameForAsset(assetData, 'of this asset');
|
||||
return `Not enough ${assetName} available`;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const errorDescription = (error?: any, assetData?: string): { icon: string; message: string } => {
|
||||
let bestMessage: string | undefined;
|
||||
if (error instanceof Error) {
|
||||
bestMessage = humanReadableMessageForError(error, assetData);
|
||||
}
|
||||
return {
|
||||
icon: '😢',
|
||||
message: bestMessage || 'Something went wrong...',
|
||||
};
|
||||
};
|
||||
27
packages/instant/src/util/error_flasher.ts
Normal file
27
packages/instant/src/util/error_flasher.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Dispatch } from 'redux';
|
||||
|
||||
import { Action, actions } from '../redux/actions';
|
||||
|
||||
class ErrorFlasher {
|
||||
private _timeoutId?: number;
|
||||
public flashNewError(dispatch: Dispatch<Action>, error: any, delayMs: number = 7000): void {
|
||||
this._clearTimeout();
|
||||
|
||||
// dispatch new message
|
||||
dispatch(actions.setError(error));
|
||||
|
||||
this._timeoutId = window.setTimeout(() => {
|
||||
dispatch(actions.hideError());
|
||||
}, delayMs);
|
||||
}
|
||||
public clearError(dispatch: Dispatch<Action>): void {
|
||||
this._clearTimeout();
|
||||
dispatch(actions.hideError());
|
||||
}
|
||||
private _clearTimeout(): void {
|
||||
if (this._timeoutId) {
|
||||
window.clearTimeout(this._timeoutId);
|
||||
}
|
||||
}
|
||||
}
|
||||
export const errorFlasher = new ErrorFlasher();
|
||||
Reference in New Issue
Block a user