aboutsummaryrefslogtreecommitdiffstats
path: root/packages/instant
diff options
context:
space:
mode:
Diffstat (limited to 'packages/instant')
-rw-r--r--packages/instant/package.json1
-rw-r--r--packages/instant/src/components/animations/slide_animations.tsx54
-rw-r--r--packages/instant/src/components/animations/slide_up_and_down_animation.tsx96
-rw-r--r--packages/instant/src/components/asset_amount_input.tsx20
-rw-r--r--packages/instant/src/components/instant_heading.tsx38
-rw-r--r--packages/instant/src/components/sliding_error.tsx24
-rw-r--r--packages/instant/src/components/zero_ex_instant_container.tsx4
-rw-r--r--packages/instant/src/containers/latest_error.tsx35
-rw-r--r--packages/instant/src/containers/selected_asset_amount_input.ts35
-rw-r--r--packages/instant/src/containers/selected_asset_buy_button.ts8
-rw-r--r--packages/instant/src/containers/selected_asset_instant_heading.ts3
-rw-r--r--packages/instant/src/redux/actions.ts12
-rw-r--r--packages/instant/src/redux/reducer.ts46
-rw-r--r--packages/instant/src/redux/store.ts3
-rw-r--r--packages/instant/src/types.ts8
-rw-r--r--packages/instant/src/util/asset_data.ts21
-rw-r--r--packages/instant/src/util/error.ts62
-rw-r--r--packages/instant/src/util/format.ts20
-rw-r--r--packages/instant/test/util/asset_data.test.ts17
-rw-r--r--packages/instant/test/util/error.test.ts48
20 files changed, 400 insertions, 155 deletions
diff --git a/packages/instant/package.json b/packages/instant/package.json
index 0c4b470fa..5c4436c52 100644
--- a/packages/instant/package.json
+++ b/packages/instant/package.json
@@ -55,6 +55,7 @@
"react-dom": "^16.5.2",
"react-redux": "^5.0.7",
"redux": "^4.0.0",
+ "redux-devtools-extension": "^2.13.5",
"styled-components": "^3.4.9",
"ts-optchain": "^0.1.1"
},
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 9c18e0933..000000000
--- a/packages/instant/src/components/animations/slide_up_and_down_animation.tsx
+++ /dev/null
@@ -1,96 +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..db3dbe7f3 100644
--- a/packages/instant/src/components/asset_amount_input.tsx
+++ b/packages/instant/src/components/asset_amount_input.tsx
@@ -1,9 +1,9 @@
-import { AssetProxyId } from '@0xproject/types';
import { BigNumber } from '@0xproject/utils';
import * as _ from 'lodash';
import * as React from 'react';
-import { assetMetaData } from '../data/asset_meta_data';
+import { assetDataUtil } from '../util/asset_data';
+
import { ColorOption } from '../style/theme';
import { util } from '../util/util';
@@ -26,26 +26,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()}
+ {assetDataUtil.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/instant_heading.tsx b/packages/instant/src/components/instant_heading.tsx
index 492c1b2c0..f57b59b73 100644
--- a/packages/instant/src/components/instant_heading.tsx
+++ b/packages/instant/src/components/instant_heading.tsx
@@ -4,6 +4,7 @@ import * as React from 'react';
import { SelectedAssetAmountInput } from '../containers/selected_asset_amount_input';
import { ColorOption } from '../style/theme';
+import { AsyncProcessState } from '../types';
import { format } from '../util/format';
import { Container, Flex, Text } from './ui';
@@ -12,20 +13,45 @@ export interface InstantHeadingProps {
selectedAssetAmount?: BigNumber;
totalEthBaseAmount?: BigNumber;
ethUsdPrice?: BigNumber;
+ quoteState: AsyncProcessState;
}
-const displaytotalEthBaseAmount = ({ selectedAssetAmount, totalEthBaseAmount }: InstantHeadingProps): string => {
+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, '...loading');
+ return format.ethBaseAmount(totalEthBaseAmount, 4, <Placeholder />);
};
-const displayUsdAmount = ({ totalEthBaseAmount, selectedAssetAmount, ethUsdPrice }: InstantHeadingProps): string => {
+const displayUsdAmount = ({
+ totalEthBaseAmount,
+ selectedAssetAmount,
+ ethUsdPrice,
+}: InstantHeadingProps): React.ReactNode => {
if (_.isUndefined(selectedAssetAmount)) {
return '$0.00';
}
- return format.ethBaseAmountInUsd(totalEthBaseAmount, ethUsdPrice, 2, '...loading');
+ return format.ethBaseAmountInUsd(totalEthBaseAmount, ethUsdPrice, 2, <Placeholder />);
+};
+
+const loadingOrAmount = (quoteState: AsyncProcessState, amount: React.ReactNode): React.ReactNode => {
+ if (quoteState === AsyncProcessState.PENDING) {
+ return (
+ <Text fontWeight="bold" fontColor={ColorOption.white}>
+ &hellip;loading
+ </Text>
+ );
+ } else {
+ return amount;
+ }
};
export const InstantHeading: React.StatelessComponent<InstantHeadingProps> = props => (
@@ -47,11 +73,11 @@ export const InstantHeading: React.StatelessComponent<InstantHeadingProps> = pro
<Flex direction="column" justify="space-between">
<Container marginBottom="5px">
<Text fontSize="16px" fontColor={ColorOption.white} fontWeight={500}>
- {displaytotalEthBaseAmount(props)}
+ {loadingOrAmount(props.quoteState, displaytotalEthBaseAmount(props))}
</Text>
</Container>
<Text fontSize="16px" fontColor={ColorOption.white} opacity={0.7}>
- {displayUsdAmount(props)}
+ {loadingOrAmount(props.quoteState, displayUsdAmount(props))}
</Text>
</Flex>
</Flex>
diff --git a/packages/instant/src/components/sliding_error.tsx b/packages/instant/src/components/sliding_error.tsx
index 0237fb7e9..3865a8797 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';
@@ -20,8 +20,8 @@ export const Error: React.StatelessComponent<ErrorProps> = props => (
borderRadius="6px"
marginBottom="10px"
>
- <Container marginRight="5px" display="inline">
- {props.icon}
+ <Container marginRight="5px" display="inline" top="3px" position="relative">
+ <Text fontSize="20px">{props.icon}</Text>
</Container>
<Text fontWeight="500" fontColor={ColorOption.darkOrange}>
{props.message}
@@ -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 51f9dc63e..cf918d890 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';
@@ -12,6 +13,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..08ea418e7
--- /dev/null
+++ b/packages/instant/src/containers/latest_error.tsx
@@ -0,0 +1,35 @@
+import * as React from 'react';
+
+import { connect } from 'react-redux';
+
+import { SlidingError } from '../components/sliding_error';
+import { LatestErrorDisplay, State } from '../redux/reducer';
+import { errorUtil } from '../util/error';
+
+export interface LatestErrorComponentProps {
+ assetData?: string;
+ latestError?: any;
+ slidingDirection: 'down' | 'up';
+}
+
+export const LatestErrorComponent: React.StatelessComponent<LatestErrorComponentProps> = props => {
+ if (!props.latestError) {
+ return <div />;
+ }
+ const { icon, message } = errorUtil.errorDescription(props.latestError, props.assetData);
+ return <SlidingError direction={props.slidingDirection} icon={icon} message={message} />;
+};
+
+interface ConnectedState {
+ assetData?: string;
+ latestError?: any;
+ slidingDirection: 'down' | 'up';
+}
+export interface LatestErrorProps {}
+const mapStateToProps = (state: State, _ownProps: LatestErrorProps): ConnectedState => ({
+ assetData: state.selectedAssetData,
+ latestError: state.latestError,
+ slidingDirection: state.latestErrorDisplay === LatestErrorDisplay.Present ? 'up' : 'down',
+});
+
+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..0873f1ab6 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 { errorUtil } from '../util/error';
import { AssetAmountInput } from '../components/asset_amount_input';
@@ -35,15 +37,25 @@ const mapStateToProps = (state: State, _ownProps: SelectedAssetAmountInputProps)
const updateBuyQuoteAsync = async (
dispatch: Dispatch<Action>,
- assetData?: string,
- assetAmount?: BigNumber,
+ assetData: string,
+ assetAmount: BigNumber,
): Promise<void> => {
- if (_.isUndefined(assetAmount) || _.isUndefined(assetData)) {
- return;
- }
// get a new buy quote.
const baseUnitValue = Web3Wrapper.toBaseUnitAmount(assetAmount, zrxDecimals);
- const newBuyQuote = await assetBuyer.getBuyQuoteAsync(assetData, baseUnitValue);
+
+ // mark quote as pending
+ dispatch(actions.updateBuyQuoteStatePending());
+
+ let newBuyQuote: BuyQuote | undefined;
+ try {
+ newBuyQuote = await assetBuyer.getBuyQuoteAsync(assetData, baseUnitValue);
+ } catch (error) {
+ dispatch(actions.updateBuyQuoteStateFailure());
+ errorUtil.errorFlasher.flashNewError(dispatch, error);
+ return;
+ }
+ // We have a successful new buy quote
+ errorUtil.errorFlasher.clearError(dispatch);
// invalidate the last buy quote.
dispatch(actions.updateLatestBuyQuote(newBuyQuote));
};
@@ -60,9 +72,14 @@ const mapDispatchToProps = (
// invalidate the last buy quote.
dispatch(actions.updateLatestBuyQuote(undefined));
// reset our buy state
- dispatch(actions.updateSelectedAssetBuyState(AsyncProcessState.NONE));
- // tslint:disable-next-line:no-floating-promises
- debouncedUpdateBuyQuoteAsync(dispatch, assetData, value);
+ 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());
+ // tslint:disable-next-line:no-floating-promises
+ debouncedUpdateBuyQuoteAsync(dispatch, assetData, value);
+ }
},
});
diff --git a/packages/instant/src/containers/selected_asset_buy_button.ts b/packages/instant/src/containers/selected_asset_buy_button.ts
index 4cbaf5537..7d3919468 100644
--- a/packages/instant/src/containers/selected_asset_buy_button.ts
+++ b/packages/instant/src/containers/selected_asset_buy_button.ts
@@ -39,14 +39,14 @@ const textForState = (state: AsyncProcessState): string => {
};
const mapStateToProps = (state: State, _ownProps: SelectedAssetBuyButtonProps): ConnectedState => ({
- text: textForState(state.selectedAssetBuyState),
+ text: textForState(state.buyOrderState),
buyQuote: state.latestBuyQuote,
});
const mapDispatchToProps = (dispatch: Dispatch<Action>, ownProps: SelectedAssetBuyButtonProps): ConnectedDispatch => ({
- onClick: buyQuote => dispatch(actions.updateSelectedAssetBuyState(AsyncProcessState.PENDING)),
- onBuySuccess: buyQuote => dispatch(actions.updateSelectedAssetBuyState(AsyncProcessState.SUCCESS)),
- onBuyFailure: buyQuote => dispatch(actions.updateSelectedAssetBuyState(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(
diff --git a/packages/instant/src/containers/selected_asset_instant_heading.ts b/packages/instant/src/containers/selected_asset_instant_heading.ts
index c97cfe11a..be31527f1 100644
--- a/packages/instant/src/containers/selected_asset_instant_heading.ts
+++ b/packages/instant/src/containers/selected_asset_instant_heading.ts
@@ -5,6 +5,7 @@ import { connect } from 'react-redux';
import { oc } from 'ts-optchain';
import { State } from '../redux/reducer';
+import { AsyncProcessState } from '../types';
import { InstantHeading } from '../components/instant_heading';
@@ -14,12 +15,14 @@ interface ConnectedState {
selectedAssetAmount?: BigNumber;
totalEthBaseAmount?: BigNumber;
ethUsdPrice?: BigNumber;
+ quoteState: AsyncProcessState;
}
const mapStateToProps = (state: State, _ownProps: InstantHeadingProps): ConnectedState => ({
selectedAssetAmount: state.selectedAssetAmount,
totalEthBaseAmount: oc(state).latestBuyQuote.worstCaseQuoteInfo.totalEthAmount(),
ethUsdPrice: state.ethUsdPrice,
+ quoteState: state.quoteState,
});
export const SelectedAssetInstantHeading: React.ComponentClass<InstantHeadingProps> = connect(mapStateToProps)(
diff --git a/packages/instant/src/redux/actions.ts b/packages/instant/src/redux/actions.ts
index 7d07b4950..bec847742 100644
--- a/packages/instant/src/redux/actions.ts
+++ b/packages/instant/src/redux/actions.ts
@@ -25,12 +25,22 @@ 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_ERROR = 'SET_ERROR',
+ HIDE_ERROR = 'HIDE_ERROR',
+ CLEAR_ERROR = 'CLEAR_ERROR',
}
export const actions = {
updateEthUsdPrice: (price?: BigNumber) => createAction(ActionTypes.UPDATE_ETH_USD_PRICE, price),
updateSelectedAssetAmount: (amount?: BigNumber) => createAction(ActionTypes.UPDATE_SELECTED_ASSET_AMOUNT, amount),
- updateSelectedAssetBuyState: (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),
+ 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..54290483b 100644
--- a/packages/instant/src/redux/reducer.ts
+++ b/packages/instant/src/redux/reducer.ts
@@ -7,21 +7,31 @@ import { AsyncProcessState } from '../types';
import { Action, ActionTypes } from './actions';
+export enum LatestErrorDisplay {
+ Present,
+ Hidden,
+}
export interface State {
selectedAssetData?: string;
selectedAssetAmount?: BigNumber;
- selectedAssetBuyState: AsyncProcessState;
+ buyOrderState: AsyncProcessState;
ethUsdPrice?: BigNumber;
latestBuyQuote?: BuyQuote;
+ quoteState: AsyncProcessState;
+ latestError?: any;
+ latestErrorDisplay: LatestErrorDisplay;
}
export const INITIAL_STATE: State = {
// TODO: Remove hardcoded zrxAssetData
selectedAssetData: zrxAssetData,
selectedAssetAmount: undefined,
- selectedAssetBuyState: AsyncProcessState.NONE,
+ buyOrderState: AsyncProcessState.NONE,
ethUsdPrice: undefined,
latestBuyQuote: undefined,
+ latestError: undefined,
+ latestErrorDisplay: LatestErrorDisplay.Hidden,
+ quoteState: AsyncProcessState.NONE,
};
export const reducer = (state: State = INITIAL_STATE, action: Action): State => {
@@ -40,11 +50,41 @@ export const reducer = (state: State = INITIAL_STATE, action: Action): State =>
return {
...state,
latestBuyQuote: action.data,
+ quoteState: AsyncProcessState.SUCCESS,
+ };
+ case ActionTypes.UPDATE_BUY_QUOTE_STATE_PENDING:
+ return {
+ ...state,
+ latestBuyQuote: undefined,
+ quoteState: AsyncProcessState.PENDING,
+ };
+ case ActionTypes.UPDATE_BUY_QUOTE_STATE_FAILURE:
+ return {
+ ...state,
+ latestBuyQuote: undefined,
+ quoteState: AsyncProcessState.FAILURE,
};
case ActionTypes.UPDATE_SELECTED_ASSET_BUY_STATE:
return {
...state,
- selectedAssetBuyState: action.data,
+ buyOrderState: action.data,
+ };
+ case ActionTypes.SET_ERROR:
+ return {
+ ...state,
+ latestError: action.data,
+ latestErrorDisplay: LatestErrorDisplay.Present,
+ };
+ case ActionTypes.HIDE_ERROR:
+ return {
+ ...state,
+ latestErrorDisplay: LatestErrorDisplay.Hidden,
+ };
+ case ActionTypes.CLEAR_ERROR:
+ return {
+ ...state,
+ latestError: undefined,
+ latestErrorDisplay: LatestErrorDisplay.Hidden,
};
default:
return state;
diff --git a/packages/instant/src/redux/store.ts b/packages/instant/src/redux/store.ts
index fcd19f9a8..b9ce9c0c1 100644
--- a/packages/instant/src/redux/store.ts
+++ b/packages/instant/src/redux/store.ts
@@ -1,6 +1,7 @@
import * as _ from 'lodash';
import { createStore, Store as ReduxStore } from 'redux';
+import { devToolsEnhancer } from 'redux-devtools-extension/developmentOnly';
import { reducer, State } from './reducer';
-export const store: ReduxStore<State> = createStore(reducer);
+export const store: ReduxStore<State> = createStore(reducer, devToolsEnhancer({}));
diff --git a/packages/instant/src/types.ts b/packages/instant/src/types.ts
index bf3ee392f..f0ffb893b 100644
--- a/packages/instant/src/types.ts
+++ b/packages/instant/src/types.ts
@@ -2,10 +2,10 @@ import { AssetProxyId, ObjectMap } from '@0xproject/types';
// Reusable
export enum AsyncProcessState {
- NONE,
- PENDING,
- SUCCESS,
- FAILURE,
+ NONE = 'None',
+ PENDING = 'Pending',
+ SUCCESS = 'Success',
+ FAILURE = 'Failure',
}
export type FunctionType = (...args: any[]) => any;
diff --git a/packages/instant/src/util/asset_data.ts b/packages/instant/src/util/asset_data.ts
new file mode 100644
index 000000000..f7c5b78cd
--- /dev/null
+++ b/packages/instant/src/util/asset_data.ts
@@ -0,0 +1,21 @@
+import * as _ from 'lodash';
+
+import { AssetProxyId } from '@0xproject/types';
+
+import { assetMetaData } from '../data/asset_meta_data';
+
+export const assetDataUtil = {
+ bestNameForAsset: (assetData: string | undefined, defaultString: string) => {
+ if (_.isUndefined(assetData)) {
+ return defaultString;
+ }
+ const metaData = assetMetaData[assetData];
+ if (_.isUndefined(metaData)) {
+ return defaultString;
+ }
+ if (metaData.assetProxyId === AssetProxyId.ERC20) {
+ return metaData.symbol.toUpperCase();
+ }
+ return defaultString;
+ },
+};
diff --git a/packages/instant/src/util/error.ts b/packages/instant/src/util/error.ts
new file mode 100644
index 000000000..48cb131a9
--- /dev/null
+++ b/packages/instant/src/util/error.ts
@@ -0,0 +1,62 @@
+import { AssetBuyerError } from '@0xproject/asset-buyer';
+import { Dispatch } from 'redux';
+
+import { Action, actions } from '../redux/actions';
+import { assetDataUtil } from '../util/asset_data';
+
+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);
+ }
+ }
+}
+
+const humanReadableMessageForError = (error: Error, assetData?: string): string | undefined => {
+ const hasInsufficientLiquidity =
+ error.message === AssetBuyerError.InsufficientAssetLiquidity ||
+ error.message === AssetBuyerError.InsufficientZrxLiquidity;
+ if (hasInsufficientLiquidity) {
+ const assetName = assetDataUtil.bestNameForAsset(assetData, 'of this asset');
+ return `Not enough ${assetName} available`;
+ }
+
+ if (
+ error.message === AssetBuyerError.StandardRelayerApiError ||
+ error.message.startsWith(AssetBuyerError.AssetUnavailable)
+ ) {
+ const assetName = assetDataUtil.bestNameForAsset(assetData, 'This asset');
+ return `${assetName} is currently unavailable`;
+ }
+
+ return undefined;
+};
+
+export const errorUtil = {
+ errorFlasher: new ErrorFlasher(),
+ 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/format.ts b/packages/instant/src/util/format.ts
index b62c968fb..09eb880b2 100644
--- a/packages/instant/src/util/format.ts
+++ b/packages/instant/src/util/format.ts
@@ -5,14 +5,22 @@ import * as _ from 'lodash';
import { ethDecimals } from '../constants';
export const format = {
- ethBaseAmount: (ethBaseAmount?: BigNumber, decimalPlaces: number = 4, defaultText: string = '0 ETH'): string => {
+ ethBaseAmount: (
+ ethBaseAmount?: BigNumber,
+ decimalPlaces: number = 4,
+ defaultText: React.ReactNode = '0 ETH',
+ ): React.ReactNode => {
if (_.isUndefined(ethBaseAmount)) {
return defaultText;
}
const ethUnitAmount = Web3Wrapper.toUnitAmount(ethBaseAmount, ethDecimals);
return format.ethUnitAmount(ethUnitAmount, decimalPlaces);
},
- ethUnitAmount: (ethUnitAmount?: BigNumber, decimalPlaces: number = 4, defaultText: string = '0 ETH'): string => {
+ ethUnitAmount: (
+ ethUnitAmount?: BigNumber,
+ decimalPlaces: number = 4,
+ defaultText: React.ReactNode = '0 ETH',
+ ): React.ReactNode => {
if (_.isUndefined(ethUnitAmount)) {
return defaultText;
}
@@ -23,8 +31,8 @@ export const format = {
ethBaseAmount?: BigNumber,
ethUsdPrice?: BigNumber,
decimalPlaces: number = 2,
- defaultText: string = '$0.00',
- ): string => {
+ defaultText: React.ReactNode = '$0.00',
+ ): React.ReactNode => {
if (_.isUndefined(ethBaseAmount) || _.isUndefined(ethUsdPrice)) {
return defaultText;
}
@@ -35,8 +43,8 @@ export const format = {
ethUnitAmount?: BigNumber,
ethUsdPrice?: BigNumber,
decimalPlaces: number = 2,
- defaultText: string = '$0.00',
- ): string => {
+ defaultText: React.ReactNode = '$0.00',
+ ): React.ReactNode => {
if (_.isUndefined(ethUnitAmount) || _.isUndefined(ethUsdPrice)) {
return defaultText;
}
diff --git a/packages/instant/test/util/asset_data.test.ts b/packages/instant/test/util/asset_data.test.ts
new file mode 100644
index 000000000..cf247142a
--- /dev/null
+++ b/packages/instant/test/util/asset_data.test.ts
@@ -0,0 +1,17 @@
+import { assetDataUtil } from '../../src/util/asset_data';
+
+const ZRX_ASSET_DATA = '0xf47261b0000000000000000000000000e41d2489571d322189246dafa5ebde1f4699f498';
+
+describe('assetDataUtil', () => {
+ describe('bestNameForAsset', () => {
+ it('should return default string if assetData is undefined', () => {
+ expect(assetDataUtil.bestNameForAsset(undefined, 'xyz')).toEqual('xyz');
+ });
+ it('should return default string if assetData isnt found', () => {
+ expect(assetDataUtil.bestNameForAsset('fake', 'mah default')).toEqual('mah default');
+ });
+ it('should return ZRX for ZRX assetData', () => {
+ expect(assetDataUtil.bestNameForAsset(ZRX_ASSET_DATA, 'mah default')).toEqual('ZRX');
+ });
+ });
+});
diff --git a/packages/instant/test/util/error.test.ts b/packages/instant/test/util/error.test.ts
new file mode 100644
index 000000000..56f3a1e86
--- /dev/null
+++ b/packages/instant/test/util/error.test.ts
@@ -0,0 +1,48 @@
+import { AssetBuyerError } from '@0xproject/asset-buyer';
+
+import { errorUtil } from '../../src/util/error';
+
+const ZRX_ASSET_DATA = '0xf47261b0000000000000000000000000e41d2489571d322189246dafa5ebde1f4699f498';
+
+describe('errorUtil', () => {
+ describe('errorFlasher', () => {
+ it('should return error and asset name for InsufficientAssetLiquidity', () => {
+ const insufficientAssetError = new Error(AssetBuyerError.InsufficientAssetLiquidity);
+ expect(errorUtil.errorDescription(insufficientAssetError, ZRX_ASSET_DATA).message).toEqual(
+ 'Not enough ZRX available',
+ );
+ });
+ it('should return error default name for InsufficientAssetLiquidity', () => {
+ const insufficientZrxError = new Error(AssetBuyerError.InsufficientZrxLiquidity);
+ expect(errorUtil.errorDescription(insufficientZrxError).message).toEqual(
+ 'Not enough of this asset available',
+ );
+ });
+ it('should return asset name for InsufficientAssetLiquidity', () => {
+ const insufficientZrxError = new Error(AssetBuyerError.InsufficientZrxLiquidity);
+ expect(errorUtil.errorDescription(insufficientZrxError, ZRX_ASSET_DATA).message).toEqual(
+ 'Not enough ZRX available',
+ );
+ });
+ it('should return unavailable error and asset name for StandardRelayerApiError', () => {
+ const standardRelayerError = new Error(AssetBuyerError.StandardRelayerApiError);
+ expect(errorUtil.errorDescription(standardRelayerError, ZRX_ASSET_DATA).message).toEqual(
+ 'ZRX is currently unavailable',
+ );
+ });
+ it('should return error for AssetUnavailable error', () => {
+ const assetUnavailableError = new Error(
+ `${AssetBuyerError.AssetUnavailable}: For assetData ${ZRX_ASSET_DATA}`,
+ );
+ expect(errorUtil.errorDescription(assetUnavailableError, ZRX_ASSET_DATA).message).toEqual(
+ 'ZRX is currently unavailable',
+ );
+ });
+ it('should return default for AssetUnavailable error', () => {
+ const assetUnavailableError = new Error(`${AssetBuyerError.AssetUnavailable}: For assetData xyz`);
+ expect(errorUtil.errorDescription(assetUnavailableError, 'xyz').message).toEqual(
+ 'This asset is currently unavailable',
+ );
+ });
+ });
+});