aboutsummaryrefslogtreecommitdiffstats
path: root/packages/instant
diff options
context:
space:
mode:
authorSteve Klebanoff <steve.klebanoff@gmail.com>2018-10-17 02:25:52 +0800
committerSteve Klebanoff <steve.klebanoff@gmail.com>2018-10-18 05:44:39 +0800
commitdb77cd10c550803c4f3fac585adc0a7f6ffa8999 (patch)
treed575b28199e32fd0ebdbac831e3839cc4fa2aa10 /packages/instant
parentf36352be47a3caf92e16e3965c86b593bfc46fea (diff)
downloaddexon-0x-contracts-db77cd10c550803c4f3fac585adc0a7f6ffa8999.tar.gz
dexon-0x-contracts-db77cd10c550803c4f3fac585adc0a7f6ffa8999.tar.zst
dexon-0x-contracts-db77cd10c550803c4f3fac585adc0a7f6ffa8999.zip
feat(instant): Handle AssetBuyer errors
Diffstat (limited to 'packages/instant')
-rw-r--r--packages/instant/src/components/animations/slide_animations.tsx54
-rw-r--r--packages/instant/src/components/animations/slide_up_and_down_animation.tsx95
-rw-r--r--packages/instant/src/components/asset_amount_input.tsx19
-rw-r--r--packages/instant/src/components/sliding_error.tsx20
-rw-r--r--packages/instant/src/components/zero_ex_instant_container.tsx4
-rw-r--r--packages/instant/src/containers/latest_error.tsx36
-rw-r--r--packages/instant/src/containers/selected_asset_amount_input.ts13
-rw-r--r--packages/instant/src/redux/actions.ts6
-rw-r--r--packages/instant/src/redux/reducer.ts30
-rw-r--r--packages/instant/src/redux/store.ts3
-rw-r--r--packages/instant/src/util/asset_data.ts18
-rw-r--r--packages/instant/src/util/error_description.ts23
-rw-r--r--packages/instant/src/util/error_flasher.ts27
13 files changed, 228 insertions, 120 deletions
diff --git a/packages/instant/src/components/animations/slide_animations.tsx b/packages/instant/src/components/animations/slide_animations.tsx
new file mode 100644
index 000000000..1f10a2ed6
--- /dev/null
+++ b/packages/instant/src/components/animations/slide_animations.tsx
@@ -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>
+);
diff --git a/packages/instant/src/components/animations/slide_up_and_down_animation.tsx b/packages/instant/src/components/animations/slide_up_and_down_animation.tsx
deleted file mode 100644
index 5fa0b0eda..000000000
--- a/packages/instant/src/components/animations/slide_up_and_down_animation.tsx
+++ /dev/null
@@ -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>;
- }
-}
diff --git a/packages/instant/src/components/asset_amount_input.tsx b/packages/instant/src/components/asset_amount_input.tsx
index 7c6b03ee9..a7df2da4d 100644
--- a/packages/instant/src/components/asset_amount_input.tsx
+++ b/packages/instant/src/components/asset_amount_input.tsx
@@ -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);
};
diff --git a/packages/instant/src/components/sliding_error.tsx b/packages/instant/src/components/sliding_error.tsx
index 0237fb7e9..ad87481c4 100644
--- a/packages/instant/src/components/sliding_error.tsx
+++ b/packages/instant/src/components/sliding_error.tsx
@@ -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>
+ );
+};
diff --git a/packages/instant/src/components/zero_ex_instant_container.tsx b/packages/instant/src/components/zero_ex_instant_container.tsx
index 0c37e41db..cc718d200 100644
--- a/packages/instant/src/components/zero_ex_instant_container.tsx
+++ b/packages/instant/src/components/zero_ex_instant_container.tsx
@@ -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"
diff --git a/packages/instant/src/containers/latest_error.tsx b/packages/instant/src/containers/latest_error.tsx
new file mode 100644
index 000000000..5272d9610
--- /dev/null
+++ b/packages/instant/src/containers/latest_error.tsx
@@ -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);
diff --git a/packages/instant/src/containers/selected_asset_amount_input.ts b/packages/instant/src/containers/selected_asset_amount_input.ts
index f2ca96ae4..00c0a1114 100644
--- a/packages/instant/src/containers/selected_asset_amount_input.ts
+++ b/packages/instant/src/containers/selected_asset_amount_input.ts
@@ -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));
};
diff --git a/packages/instant/src/redux/actions.ts b/packages/instant/src/redux/actions.ts
index 7d07b4950..cf5b39790 100644
--- a/packages/instant/src/redux/actions.ts
+++ b/packages/instant/src/redux/actions.ts
@@ -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),
};
diff --git a/packages/instant/src/redux/reducer.ts b/packages/instant/src/redux/reducer.ts
index adecf2ab7..4ff49c225 100644
--- a/packages/instant/src/redux/reducer.ts
+++ b/packages/instant/src/redux/reducer.ts
@@ -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;
}
diff --git a/packages/instant/src/redux/store.ts b/packages/instant/src/redux/store.ts
index fcd19f9a8..8d9fe34cb 100644
--- a/packages/instant/src/redux/store.ts
+++ b/packages/instant/src/redux/store.ts
@@ -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());
diff --git a/packages/instant/src/util/asset_data.ts b/packages/instant/src/util/asset_data.ts
new file mode 100644
index 000000000..958f500bb
--- /dev/null
+++ b/packages/instant/src/util/asset_data.ts
@@ -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;
+};
diff --git a/packages/instant/src/util/error_description.ts b/packages/instant/src/util/error_description.ts
new file mode 100644
index 000000000..78af9e9ff
--- /dev/null
+++ b/packages/instant/src/util/error_description.ts
@@ -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...',
+ };
+};
diff --git a/packages/instant/src/util/error_flasher.ts b/packages/instant/src/util/error_flasher.ts
new file mode 100644
index 000000000..f43c4211b
--- /dev/null
+++ b/packages/instant/src/util/error_flasher.ts
@@ -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();