diff options
author | Steve Klebanoff <steve.klebanoff@gmail.com> | 2018-10-17 02:25:52 +0800 |
---|---|---|
committer | Steve Klebanoff <steve.klebanoff@gmail.com> | 2018-10-18 05:44:39 +0800 |
commit | db77cd10c550803c4f3fac585adc0a7f6ffa8999 (patch) | |
tree | d575b28199e32fd0ebdbac831e3839cc4fa2aa10 | |
parent | f36352be47a3caf92e16e3965c86b593bfc46fea (diff) | |
download | dexon-0x-contracts-db77cd10c550803c4f3fac585adc0a7f6ffa8999.tar dexon-0x-contracts-db77cd10c550803c4f3fac585adc0a7f6ffa8999.tar.gz dexon-0x-contracts-db77cd10c550803c4f3fac585adc0a7f6ffa8999.tar.bz2 dexon-0x-contracts-db77cd10c550803c4f3fac585adc0a7f6ffa8999.tar.lz dexon-0x-contracts-db77cd10c550803c4f3fac585adc0a7f6ffa8999.tar.xz dexon-0x-contracts-db77cd10c550803c4f3fac585adc0a7f6ffa8999.tar.zst dexon-0x-contracts-db77cd10c550803c4f3fac585adc0a7f6ffa8999.zip |
feat(instant): Handle AssetBuyer errors
-rw-r--r-- | packages/instant/src/components/animations/slide_animations.tsx | 54 | ||||
-rw-r--r-- | packages/instant/src/components/animations/slide_up_and_down_animation.tsx | 95 | ||||
-rw-r--r-- | packages/instant/src/components/asset_amount_input.tsx | 19 | ||||
-rw-r--r-- | packages/instant/src/components/sliding_error.tsx | 20 | ||||
-rw-r--r-- | packages/instant/src/components/zero_ex_instant_container.tsx | 4 | ||||
-rw-r--r-- | packages/instant/src/containers/latest_error.tsx | 36 | ||||
-rw-r--r-- | packages/instant/src/containers/selected_asset_amount_input.ts | 13 | ||||
-rw-r--r-- | packages/instant/src/redux/actions.ts | 6 | ||||
-rw-r--r-- | packages/instant/src/redux/reducer.ts | 30 | ||||
-rw-r--r-- | packages/instant/src/redux/store.ts | 3 | ||||
-rw-r--r-- | packages/instant/src/util/asset_data.ts | 18 | ||||
-rw-r--r-- | packages/instant/src/util/error_description.ts | 23 | ||||
-rw-r--r-- | packages/instant/src/util/error_flasher.ts | 27 |
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(); |