diff options
Diffstat (limited to 'packages/instant')
32 files changed, 439 insertions, 202 deletions
diff --git a/packages/instant/README.md b/packages/instant/README.md index 25aca9d3b..55b4404e7 100644 --- a/packages/instant/README.md +++ b/packages/instant/README.md @@ -1,9 +1,9 @@ -## @0xproject/instant +## @0x/instant ## Installation ```bash -yarn add @0xproject/instant +yarn add @0x/instant ``` **Import** @@ -11,20 +11,20 @@ yarn add @0xproject/instant **CommonJS module** ```typescript -import { ZeroExInstant } from '@0xproject/instant'; +import { ZeroExInstant } from '@0x/instant'; ``` or ```javascript -var ZeroExInstant = require('@0xproject/instant').ZeroExInstant; +var ZeroExInstant = require('@0x/instant').ZeroExInstant; ``` If your project is in [TypeScript](https://www.typescriptlang.org/), add the following to your `tsconfig.json`: ```json "compilerOptions": { - "typeRoots": ["node_modules/@0xproject/typescript-typings/types", "node_modules/@types"], + "typeRoots": ["node_modules/@0x/typescript-typings/types", "node_modules/@types"], } ``` @@ -71,13 +71,13 @@ yarn install To build this package and all other monorepo packages that it depends on, run the following from the monorepo root directory: ```bash -PKG=@0xproject/instant yarn build +PKG=@0x/instant yarn build ``` Or continuously rebuild on change: ```bash -PKG=@0xproject/instant yarn watch +PKG=@0x/instant yarn watch ``` ### Clean diff --git a/packages/instant/package.json b/packages/instant/package.json index ec5cbebb4..e5c4ee12d 100644 --- a/packages/instant/package.json +++ b/packages/instant/package.json @@ -1,6 +1,6 @@ { - "name": "@0xproject/instant", - "version": "0.0.2", + "name": "@0x/instant", + "version": "0.0.3", "engines": { "node": ">=6.12" }, @@ -43,24 +43,25 @@ }, "homepage": "https://github.com/0xProject/0x-monorepo/packages/instant/README.md", "dependencies": { - "@0xproject/asset-buyer": "^2.0.0", - "@0xproject/order-utils": "^1.0.7", - "@0xproject/types": "^1.1.4", - "@0xproject/typescript-typings": "^2.0.2", - "@0xproject/utils": "^2.0.2", - "@0xproject/web3-wrapper": "^3.0.3", - "ethereum-types": "^1.0.11", + "@0x/asset-buyer": "^2.1.0", + "@0x/order-utils": "^2.0.0", + "@0x/types": "^1.2.0", + "@0x/typescript-typings": "^3.0.3", + "@0x/utils": "^2.0.3", + "@0x/web3-wrapper": "^3.1.0", + "ethereum-types": "^1.1.1", "lodash": "^4.17.10", "polished": "^2.2.0", "react": "^16.5.2", "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" }, "devDependencies": { - "@0xproject/tslint-config": "^1.0.8", + "@0x/tslint-config": "^1.0.9", "@types/enzyme": "^3.1.14", "@types/enzyme-adapter-react-16": "^1.0.3", "@types/jest": "^23.3.5", diff --git a/packages/instant/src/components/amount_input.tsx b/packages/instant/src/components/amount_input.tsx index 7644f5f67..c89fb05ad 100644 --- a/packages/instant/src/components/amount_input.tsx +++ b/packages/instant/src/components/amount_input.tsx @@ -1,4 +1,4 @@ -import { BigNumber } from '@0xproject/utils'; +import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; import * as React from 'react'; 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 9f4b5861a..c03ef1cf3 100644 --- a/packages/instant/src/components/asset_amount_input.tsx +++ b/packages/instant/src/components/asset_amount_input.tsx @@ -1,10 +1,10 @@ -import { BigNumber } from '@0xproject/utils'; +import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; import * as React from 'react'; -import { oc } from 'ts-optchain'; import { ColorOption } from '../style/theme'; import { ERC20Asset } from '../types'; +import { assetUtils } from '../util/asset'; import { util } from '../util/util'; import { AmountInput, AmountInputProps } from './amount_input'; @@ -27,7 +27,7 @@ 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"> - {oc(asset).metaData.symbol()} + {assetUtils.bestNameForAsset(asset)} </Text> </Container> </Container> diff --git a/packages/instant/src/components/buy_button.tsx b/packages/instant/src/components/buy_button.tsx index 191426be1..3ef7c1f5c 100644 --- a/packages/instant/src/components/buy_button.tsx +++ b/packages/instant/src/components/buy_button.tsx @@ -1,4 +1,4 @@ -import { AssetBuyer, BuyQuote } from '@0xproject/asset-buyer'; +import { AssetBuyer, BuyQuote } from '@0x/asset-buyer'; import * as _ from 'lodash'; import * as React from 'react'; diff --git a/packages/instant/src/components/instant_heading.tsx b/packages/instant/src/components/instant_heading.tsx index 492c1b2c0..a36d35a93 100644 --- a/packages/instant/src/components/instant_heading.tsx +++ b/packages/instant/src/components/instant_heading.tsx @@ -1,9 +1,10 @@ -import { BigNumber } from '@0xproject/utils'; +import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; 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}> + — + </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}> + …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/order_details.tsx b/packages/instant/src/components/order_details.tsx index a15ff411b..ad4a87714 100644 --- a/packages/instant/src/components/order_details.tsx +++ b/packages/instant/src/components/order_details.tsx @@ -1,5 +1,5 @@ -import { BuyQuoteInfo } from '@0xproject/asset-buyer'; -import { BigNumber } from '@0xproject/utils'; +import { BuyQuoteInfo } from '@0x/asset-buyer'; +import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; import * as React from 'react'; import { oc } from 'ts-optchain'; 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.tsx b/packages/instant/src/components/zero_ex_instant.tsx index cc7b0ecb8..5b75a7556 100644 --- a/packages/instant/src/components/zero_ex_instant.tsx +++ b/packages/instant/src/components/zero_ex_instant.tsx @@ -1,5 +1,5 @@ -import { AssetBuyer } from '@0xproject/asset-buyer'; -import { ObjectMap } from '@0xproject/types'; +import { AssetBuyer } from '@0x/asset-buyer'; +import { ObjectMap } from '@0x/types'; import * as React from 'react'; import { Provider } from 'react-redux'; 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/constants.ts b/packages/instant/src/constants.ts index b27378d4c..7f4c5a058 100644 --- a/packages/instant/src/constants.ts +++ b/packages/instant/src/constants.ts @@ -1,3 +1,3 @@ -import { BigNumber } from '@0xproject/utils'; +import { BigNumber } from '@0x/utils'; export const BIG_NUMBER_ZERO = new BigNumber(0); export const ethDecimals = 18; diff --git a/packages/instant/src/containers/latest_buy_quote_order_details.ts b/packages/instant/src/containers/latest_buy_quote_order_details.ts index b354c78fa..597bf3088 100644 --- a/packages/instant/src/containers/latest_buy_quote_order_details.ts +++ b/packages/instant/src/containers/latest_buy_quote_order_details.ts @@ -1,5 +1,5 @@ -import { BuyQuoteInfo } from '@0xproject/asset-buyer'; -import { BigNumber } from '@0xproject/utils'; +import { BuyQuoteInfo } from '@0x/asset-buyer'; +import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; import * as React from 'react'; import { connect } from 'react-redux'; diff --git a/packages/instant/src/containers/latest_error.tsx b/packages/instant/src/containers/latest_error.tsx new file mode 100644 index 000000000..1d02cab23 --- /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 { LatestErrorDisplay, State } from '../redux/reducer'; +import { Asset } from '../types'; +import { errorUtil } from '../util/error'; + +export interface LatestErrorComponentProps { + asset?: Asset; + 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.asset); + return <SlidingError direction={props.slidingDirection} icon={icon} message={message} />; +}; + +interface ConnectedState { + asset?: Asset; + latestError?: any; + slidingDirection: 'down' | 'up'; +} +export interface LatestErrorProps {} +const mapStateToProps = (state: State, _ownProps: LatestErrorProps): ConnectedState => ({ + asset: state.selectedAsset, + 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 b75a22a0e..6cd39b855 100644 --- a/packages/instant/src/containers/selected_asset_amount_input.ts +++ b/packages/instant/src/containers/selected_asset_amount_input.ts @@ -1,7 +1,7 @@ -import { AssetBuyer } from '@0xproject/asset-buyer'; -import { AssetProxyId } from '@0xproject/types'; -import { BigNumber } from '@0xproject/utils'; -import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import { AssetBuyer, BuyQuote } from '@0x/asset-buyer'; +import { AssetProxyId } from '@0x/types'; +import { BigNumber } from '@0x/utils'; +import { Web3Wrapper } from '@0x/web3-wrapper'; import * as _ from 'lodash'; import * as React from 'react'; import { connect } from 'react-redux'; @@ -11,6 +11,7 @@ import { Action, actions } from '../redux/actions'; import { State } from '../redux/reducer'; import { ColorOption } from '../style/theme'; import { AsyncProcessState, ERC20Asset } from '../types'; +import { errorUtil } from '../util/error'; import { AssetAmountInput } from '../components/asset_amount_input'; @@ -39,7 +40,7 @@ type FinalProps = ConnectedProps & SelectedAssetAmountInputProps; const mapStateToProps = (state: State, _ownProps: SelectedAssetAmountInputProps): ConnectedState => { const selectedAsset = state.selectedAsset; - if (_.isUndefined(selectedAsset) || selectedAsset.assetProxyId !== AssetProxyId.ERC20) { + if (_.isUndefined(selectedAsset) || selectedAsset.metaData.assetProxyId !== AssetProxyId.ERC20) { return { value: state.selectedAssetAmount, }; @@ -52,22 +53,27 @@ const mapStateToProps = (state: State, _ownProps: SelectedAssetAmountInputProps) }; const updateBuyQuoteAsync = async ( + assetBuyer: AssetBuyer, dispatch: Dispatch<Action>, - assetBuyer?: AssetBuyer, - asset?: ERC20Asset, - assetAmount?: BigNumber, + asset: ERC20Asset, + assetAmount: BigNumber, ): Promise<void> => { - if ( - _.isUndefined(assetBuyer) || - _.isUndefined(assetAmount) || - _.isUndefined(asset) || - _.isUndefined(asset.metaData) - ) { - return; - } // get a new buy quote. const baseUnitValue = Web3Wrapper.toBaseUnitAmount(assetAmount, asset.metaData.decimals); - const newBuyQuote = await assetBuyer.getBuyQuoteAsync(asset.assetData, baseUnitValue); + + // mark quote as pending + dispatch(actions.updateBuyQuoteStatePending()); + + let newBuyQuote: BuyQuote | undefined; + try { + newBuyQuote = await assetBuyer.getBuyQuoteAsync(asset.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)); }; @@ -84,9 +90,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, assetBuyer, asset, value); + dispatch(actions.updatebuyOrderState(AsyncProcessState.NONE)); + + if (!_.isUndefined(value) && !_.isUndefined(asset) && !_.isUndefined(assetBuyer)) { + // even if it's debounced, give them the illusion it's loading + dispatch(actions.updateBuyQuoteStatePending()); + // tslint:disable-next-line:no-floating-promises + debouncedUpdateBuyQuoteAsync(assetBuyer, dispatch, asset, value); + } }, }); diff --git a/packages/instant/src/containers/selected_asset_buy_button.ts b/packages/instant/src/containers/selected_asset_buy_button.ts index 4118932b2..99f971321 100644 --- a/packages/instant/src/containers/selected_asset_buy_button.ts +++ b/packages/instant/src/containers/selected_asset_buy_button.ts @@ -1,4 +1,4 @@ -import { AssetBuyer, BuyQuote } from '@0xproject/asset-buyer'; +import { AssetBuyer, BuyQuote } from '@0x/asset-buyer'; import * as _ from 'lodash'; import * as React from 'react'; import { connect } from 'react-redux'; @@ -41,14 +41,14 @@ const textForState = (state: AsyncProcessState): string => { const mapStateToProps = (state: State, _ownProps: SelectedAssetBuyButtonProps): ConnectedState => ({ assetBuyer: state.assetBuyer, - 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..0509db5da 100644 --- a/packages/instant/src/containers/selected_asset_instant_heading.ts +++ b/packages/instant/src/containers/selected_asset_instant_heading.ts @@ -1,10 +1,11 @@ -import { BigNumber } from '@0xproject/utils'; +import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; import * as React from 'react'; 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/data/asset_meta_data_map.ts b/packages/instant/src/data/asset_meta_data_map.ts index 7d83865f1..3a820a0c4 100644 --- a/packages/instant/src/data/asset_meta_data_map.ts +++ b/packages/instant/src/data/asset_meta_data_map.ts @@ -1,4 +1,4 @@ -import { AssetProxyId, ObjectMap } from '@0xproject/types'; +import { AssetProxyId, ObjectMap } from '@0x/types'; import { AssetMetaData } from '../types'; diff --git a/packages/instant/src/redux/actions.ts b/packages/instant/src/redux/actions.ts index fe055b75f..bc75ce66c 100644 --- a/packages/instant/src/redux/actions.ts +++ b/packages/instant/src/redux/actions.ts @@ -1,5 +1,5 @@ -import { BuyQuote } from '@0xproject/asset-buyer'; -import { BigNumber } from '@0xproject/utils'; +import { BuyQuote } from '@0x/asset-buyer'; +import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; import { ActionsUnion, AsyncProcessState } from '../types'; @@ -26,13 +26,23 @@ export enum ActionTypes { UPDATE_SELECTED_ASSET_BUY_STATE = 'UPDATE_SELECTED_ASSET_BUY_STATE', UPDATE_LATEST_BUY_QUOTE = 'UPDATE_LATEST_BUY_QUOTE', UPDATE_SELECTED_ASSET = 'UPDATE_SELECTED_ASSET', + 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), updateSelectedAsset: (assetData?: string) => createAction(ActionTypes.UPDATE_SELECTED_ASSET, assetData), + 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 9922131b4..657bd0e40 100644 --- a/packages/instant/src/redux/reducer.ts +++ b/packages/instant/src/redux/reducer.ts @@ -1,6 +1,6 @@ -import { AssetBuyer, BuyQuote } from '@0xproject/asset-buyer'; -import { ObjectMap } from '@0xproject/types'; -import { BigNumber } from '@0xproject/utils'; +import { AssetBuyer, BuyQuote } from '@0x/asset-buyer'; +import { ObjectMap } from '@0x/types'; +import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; import { Asset, AssetMetaData, AsyncProcessState } from '../types'; @@ -8,22 +8,32 @@ import { assetUtils } from '../util/asset'; import { Action, ActionTypes } from './actions'; +export enum LatestErrorDisplay { + Present, + Hidden, +} export interface State { assetBuyer?: AssetBuyer; assetMetaDataMap: ObjectMap<AssetMetaData>; selectedAsset?: Asset; selectedAssetAmount?: BigNumber; - selectedAssetBuyState: AsyncProcessState; + buyOrderState: AsyncProcessState; ethUsdPrice?: BigNumber; latestBuyQuote?: BuyQuote; + quoteState: AsyncProcessState; + latestError?: any; + latestErrorDisplay: LatestErrorDisplay; } export const INITIAL_STATE: State = { selectedAssetAmount: undefined, assetMetaDataMap: {}, - selectedAssetBuyState: AsyncProcessState.NONE, + buyOrderState: AsyncProcessState.NONE, ethUsdPrice: undefined, latestBuyQuote: undefined, + latestError: undefined, + latestErrorDisplay: LatestErrorDisplay.Hidden, + quoteState: AsyncProcessState.NONE, }; // TODO: Figure out why there is an INITIAL_STATE key in the store... @@ -43,11 +53,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, }; case ActionTypes.UPDATE_SELECTED_ASSET: const newSelectedAssetData = action.data; diff --git a/packages/instant/src/redux/store.ts b/packages/instant/src/redux/store.ts index fc943f1be..505234299 100644 --- a/packages/instant/src/redux/store.ts +++ b/packages/instant/src/redux/store.ts @@ -1,5 +1,6 @@ import * as _ from 'lodash'; import { createStore, Store as ReduxStore } from 'redux'; +import { devToolsEnhancer } from 'redux-devtools-extension/developmentOnly'; import { INITIAL_STATE, reducer, State } from './reducer'; @@ -11,6 +12,7 @@ export const store = { INITIAL_STATE, ...withState, }; - return createStore(reducer, allInitialState); + return createStore(reducer, allInitialState, devToolsEnhancer({})); }, }; + diff --git a/packages/instant/src/style/util.ts b/packages/instant/src/style/util.ts index c9df0f834..3e38c4a7d 100644 --- a/packages/instant/src/style/util.ts +++ b/packages/instant/src/style/util.ts @@ -1,4 +1,4 @@ -import { ObjectMap } from '@0xproject/types'; +import { ObjectMap } from '@0x/types'; import * as _ from 'lodash'; export const cssRuleIfExists = (props: ObjectMap<any>, rule: string): string => { diff --git a/packages/instant/src/types.ts b/packages/instant/src/types.ts index 867605573..0454bceea 100644 --- a/packages/instant/src/types.ts +++ b/packages/instant/src/types.ts @@ -1,11 +1,11 @@ -import { AssetProxyId, ObjectMap } from '@0xproject/types'; +import { AssetProxyId, ObjectMap } from '@0x/types'; // Reusable export enum AsyncProcessState { - NONE, - PENDING, - SUCCESS, - FAILURE, + NONE = 'None', + PENDING = 'Pending', + SUCCESS = 'Success', + FAILURE = 'Failure', } export type FunctionType = (...args: any[]) => any; @@ -29,18 +29,15 @@ export interface ERC721AssetMetaData { export type AssetMetaData = ERC20AssetMetaData | ERC721AssetMetaData; export interface ERC20Asset { - assetProxyId: AssetProxyId.ERC20; assetData: string; metaData: ERC20AssetMetaData; } export interface ERC721Asset { - assetProxyId: AssetProxyId.ERC721; assetData: string; metaData: ERC721AssetMetaData; } export interface Asset { - assetProxyId: AssetProxyId; assetData: string; metaData: AssetMetaData; } diff --git a/packages/instant/src/util/asset.ts b/packages/instant/src/util/asset.ts index ec22276ae..edeac0da3 100644 --- a/packages/instant/src/util/asset.ts +++ b/packages/instant/src/util/asset.ts @@ -1,5 +1,5 @@ -import { assetDataUtils } from '@0xproject/order-utils'; -import { AssetProxyId, ObjectMap } from '@0xproject/types'; +import { assetDataUtils } from '@0x/order-utils'; +import { AssetProxyId, ObjectMap } from '@0x/types'; import * as _ from 'lodash'; import { assetDataNetworkMapping } from '../data/asset_data_network_mapping'; @@ -8,7 +8,6 @@ import { Asset, AssetMetaData, Network, ZeroExInstantError } from '../types'; export const assetUtils = { createAssetFromAssetData: (assetData: string, assetMetaDataMap: ObjectMap<AssetMetaData>): Asset => { return { - assetProxyId: assetDataUtils.decodeAssetProxyId(assetData), assetData, metaData: assetUtils.getMetaDataOrThrow(assetData, assetMetaDataMap), }; @@ -31,4 +30,18 @@ export const assetUtils = { } return metaData; }, + bestNameForAsset: (asset?: Asset, defaultName: string = '???'): string => { + if (_.isUndefined(asset)) { + return defaultName; + } + const metaData = asset.metaData; + switch (metaData.assetProxyId) { + case AssetProxyId.ERC20: + return metaData.symbol; + case AssetProxyId.ERC721: + return metaData.name; + default: + return defaultName; + } + }, }; diff --git a/packages/instant/src/util/coinbase_api.ts b/packages/instant/src/util/coinbase_api.ts index 94a5d3c80..080421f98 100644 --- a/packages/instant/src/util/coinbase_api.ts +++ b/packages/instant/src/util/coinbase_api.ts @@ -1,4 +1,4 @@ -import { BigNumber } from '@0xproject/utils'; +import { BigNumber } from '@0x/utils'; const baseEndpoint = 'https://api.coinbase.com/v2'; export const coinbaseApi = { diff --git a/packages/instant/src/util/error.ts b/packages/instant/src/util/error.ts new file mode 100644 index 000000000..40fd24c7e --- /dev/null +++ b/packages/instant/src/util/error.ts @@ -0,0 +1,64 @@ +import { AssetBuyerError } from '@0x/asset-buyer'; +import { Dispatch } from 'redux'; + +import { Action, actions } from '../redux/actions'; +import { Asset } from '../types'; + +import { assetUtils } from './asset'; + +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, asset?: Asset): string | undefined => { + const hasInsufficientLiquidity = + error.message === AssetBuyerError.InsufficientAssetLiquidity || + error.message === AssetBuyerError.InsufficientZrxLiquidity; + if (hasInsufficientLiquidity) { + const assetName = assetUtils.bestNameForAsset(asset, 'of this asset'); + return `Not enough ${assetName} available`; + } + + if ( + error.message === AssetBuyerError.StandardRelayerApiError || + error.message.startsWith(AssetBuyerError.AssetUnavailable) + ) { + const assetName = assetUtils.bestNameForAsset(asset, 'This asset'); + return `${assetName} is currently unavailable`; + } + + return undefined; +}; + +export const errorUtil = { + errorFlasher: new ErrorFlasher(), + errorDescription: (error?: any, asset?: Asset): { icon: string; message: string } => { + let bestMessage: string | undefined; + if (error instanceof Error) { + bestMessage = humanReadableMessageForError(error, asset); + } + 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..8482b1526 100644 --- a/packages/instant/src/util/format.ts +++ b/packages/instant/src/util/format.ts @@ -1,18 +1,26 @@ -import { BigNumber } from '@0xproject/utils'; -import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import { BigNumber } from '@0x/utils'; +import { Web3Wrapper } from '@0x/web3-wrapper'; 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/src/util/web3_wrapper.ts b/packages/instant/src/util/web3_wrapper.ts index d7e43521f..24dcd9076 100644 --- a/packages/instant/src/util/web3_wrapper.ts +++ b/packages/instant/src/util/web3_wrapper.ts @@ -1,4 +1,4 @@ -import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import { Web3Wrapper } from '@0x/web3-wrapper'; import { getProvider } from './provider'; diff --git a/packages/instant/test/util/error.test.ts b/packages/instant/test/util/error.test.ts new file mode 100644 index 000000000..b009db91a --- /dev/null +++ b/packages/instant/test/util/error.test.ts @@ -0,0 +1,56 @@ +import { AssetBuyerError } from '@0x/asset-buyer'; +import { AssetProxyId } from '@0x/types'; + +import { Asset } from '../../src/types'; +import { errorUtil } from '../../src/util/error'; + +const ZRX_ASSET_DATA = '0xf47261b0000000000000000000000000e41d2489571d322189246dafa5ebde1f4699f498'; +const ZRX_ASSET: Asset = { + assetData: ZRX_ASSET_DATA, + metaData: { + assetProxyId: AssetProxyId.ERC20, + symbol: 'zrx', + decimals: 18, + }, +}; + +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).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).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).message).toEqual( + 'ZRX is currently unavailable', + ); + }); + it('should return error for AssetUnavailable error', () => { + const assetUnavailableError = new Error(`${AssetBuyerError.AssetUnavailable}: For assetData ${ZRX_ASSET}`); + expect(errorUtil.errorDescription(assetUnavailableError, ZRX_ASSET).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, ZRX_ASSET).message).toEqual( + 'This asset is currently unavailable', + ); + }); + }); +}); diff --git a/packages/instant/test/util/format.test.ts b/packages/instant/test/util/format.test.ts index 073b86b19..141df9275 100644 --- a/packages/instant/test/util/format.test.ts +++ b/packages/instant/test/util/format.test.ts @@ -1,5 +1,5 @@ -import { BigNumber } from '@0xproject/utils'; -import { Web3Wrapper } from '@0xproject/web3-wrapper'; +import { BigNumber } from '@0x/utils'; +import { Web3Wrapper } from '@0x/web3-wrapper'; import { ethDecimals } from '../../src/constants'; import { format } from '../../src/util/format'; diff --git a/packages/instant/tslint.json b/packages/instant/tslint.json index f71532e5d..abe874a6d 100644 --- a/packages/instant/tslint.json +++ b/packages/instant/tslint.json @@ -1,5 +1,5 @@ { - "extends": ["@0xproject/tslint-config"], + "extends": ["@0x/tslint-config"], "rules": { "custom-no-magic-numbers": false, "semicolon": [true, "always", "ignore-bound-class-methods"] |