diff options
-rw-r--r-- | packages/instant/src/components/animations/position_animation.tsx | 66 | ||||
-rw-r--r-- | packages/instant/src/components/animations/slide_animation.tsx | 12 | ||||
-rw-r--r-- | packages/instant/src/components/sliding_error.tsx | 37 | ||||
-rw-r--r-- | packages/instant/src/components/sliding_panel.tsx | 3 | ||||
-rw-r--r-- | packages/instant/src/components/ui/container.tsx | 6 | ||||
-rw-r--r-- | packages/instant/src/components/zero_ex_instant_container.tsx | 2 | ||||
-rw-r--r-- | packages/instant/src/components/zero_ex_instant_provider.tsx | 4 | ||||
-rw-r--r-- | packages/instant/src/containers/selected_erc20_asset_amount_input.ts | 56 | ||||
-rw-r--r-- | packages/instant/src/redux/async_data.ts | 20 | ||||
-rw-r--r-- | packages/instant/src/style/media.ts | 38 | ||||
-rw-r--r-- | packages/instant/src/style/z_index.ts | 3 | ||||
-rw-r--r-- | packages/instant/src/util/buy_quote_updater.ts | 56 | ||||
-rw-r--r-- | packages/sra-spec/src/json-schemas.ts | 2 | ||||
-rw-r--r-- | packages/utils/src/configured_bignumber.ts | 23 |
14 files changed, 226 insertions, 102 deletions
diff --git a/packages/instant/src/components/animations/position_animation.tsx b/packages/instant/src/components/animations/position_animation.tsx index 4bb21befb..576d29c07 100644 --- a/packages/instant/src/components/animations/position_animation.tsx +++ b/packages/instant/src/components/animations/position_animation.tsx @@ -1,5 +1,6 @@ -import { Keyframes } from 'styled-components'; +import { InterpolationValue } from 'styled-components'; +import { media, OptionallyScreenSpecific, stylesForMedia } from '../../style/media'; import { css, keyframes, styled } from '../../style/theme'; export interface TransitionInfo { @@ -51,30 +52,57 @@ export interface PositionAnimationSettings { right?: TransitionInfo; timingFunction: string; duration?: string; + position?: string; } -export interface PositionAnimationProps extends PositionAnimationSettings { - position: string; +const generatePositionAnimationCss = (positionSettings: PositionAnimationSettings) => { + return css` + animation-name: ${slideKeyframeGenerator( + positionSettings.position || 'relative', + positionSettings.top, + positionSettings.bottom, + positionSettings.left, + positionSettings.right, + )}; + animation-duration: ${positionSettings.duration || '0.3s'}; + animation-timing-function: ${positionSettings.timingFunction}; + animation-delay: 0s; + animation-iteration-count: 1; + animation-fill-mode: forwards; + position: ${positionSettings.position || 'relative'}; + width: 100%; + `; +}; + +export interface PositionAnimationProps { + positionSettings: OptionallyScreenSpecific<PositionAnimationSettings>; + zIndex?: OptionallyScreenSpecific<number>; } +const defaultAnimation = (positionSettings: OptionallyScreenSpecific<PositionAnimationSettings>) => { + const bestDefault = 'default' in positionSettings ? positionSettings.default : positionSettings; + return generatePositionAnimationCss(bestDefault); +}; +const animationForSize = ( + positionSettings: OptionallyScreenSpecific<PositionAnimationSettings>, + sizeKey: 'sm' | 'md' | 'lg', + mediaFn: (...args: any[]) => InterpolationValue[], +) => { + // checking default makes sure we have a PositionAnimationSettings object + // and then we check to see if we have a setting for the specific `sizeKey` + const animationSettingsForSize = 'default' in positionSettings && positionSettings[sizeKey]; + return animationSettingsForSize && mediaFn`${generatePositionAnimationCss(animationSettingsForSize)}`; +}; + export const PositionAnimation = styled.div < PositionAnimationProps > ` - animation-name: ${props => - css` - ${slideKeyframeGenerator(props.position, props.top, props.bottom, props.left, props.right)}; - `}; - animation-duration: ${props => props.duration || '0.3s'}; - animation-timing-function: ${props => props.timingFunction}; - animation-delay: 0s; - animation-iteration-count: 1; - animation-fill-mode: forwards; - position: ${props => props.position}; - height: 100%; - width: 100%; + && { + ${props => props.zIndex && stylesForMedia<number>('z-index', props.zIndex)} + ${props => defaultAnimation(props.positionSettings)} + ${props => animationForSize(props.positionSettings, 'sm', media.small)} + ${props => animationForSize(props.positionSettings, 'md', media.medium)} + ${props => animationForSize(props.positionSettings, 'lg', media.large)} + } `; - -PositionAnimation.defaultProps = { - position: 'relative', -}; diff --git a/packages/instant/src/components/animations/slide_animation.tsx b/packages/instant/src/components/animations/slide_animation.tsx index 66a314c7f..122229dee 100644 --- a/packages/instant/src/components/animations/slide_animation.tsx +++ b/packages/instant/src/components/animations/slide_animation.tsx @@ -1,22 +1,24 @@ import * as React from 'react'; +import { OptionallyScreenSpecific } from '../../style/media'; + import { PositionAnimation, PositionAnimationSettings } from './position_animation'; export type SlideAnimationState = 'slidIn' | 'slidOut' | 'none'; export interface SlideAnimationProps { - position: string; animationState: SlideAnimationState; - slideInSettings: PositionAnimationSettings; - slideOutSettings: PositionAnimationSettings; + slideInSettings: OptionallyScreenSpecific<PositionAnimationSettings>; + slideOutSettings: OptionallyScreenSpecific<PositionAnimationSettings>; + zIndex?: OptionallyScreenSpecific<number>; } export const SlideAnimation: React.StatelessComponent<SlideAnimationProps> = props => { if (props.animationState === 'none') { return <React.Fragment>{props.children}</React.Fragment>; } - const propsToUse = props.animationState === 'slidIn' ? props.slideInSettings : props.slideOutSettings; + const positionSettings = props.animationState === 'slidIn' ? props.slideInSettings : props.slideOutSettings; return ( - <PositionAnimation position={props.position} {...propsToUse}> + <PositionAnimation positionSettings={positionSettings} zIndex={props.zIndex}> {props.children} </PositionAnimation> ); diff --git a/packages/instant/src/components/sliding_error.tsx b/packages/instant/src/components/sliding_error.tsx index a923b9932..a8d4e391c 100644 --- a/packages/instant/src/components/sliding_error.tsx +++ b/packages/instant/src/components/sliding_error.tsx @@ -1,6 +1,8 @@ import * as React from 'react'; +import { ScreenSpecification } from '../style/media'; import { ColorOption } from '../style/theme'; +import { zIndex } from '../style/z_index'; import { PositionAnimationSettings } from './animations/position_animation'; import { SlideAnimation, SlideAnimationState } from './animations/slide_animation'; @@ -21,6 +23,7 @@ export const Error: React.StatelessComponent<ErrorProps> = props => ( backgroundColor={ColorOption.lightOrange} width="100%" borderRadius="6px" + marginTop="10px" marginBottom="10px" > <Flex justify="flex-start"> @@ -39,25 +42,51 @@ export interface SlidingErrorProps extends ErrorProps { } export const SlidingError: React.StatelessComponent<SlidingErrorProps> = props => { const slideAmount = '120px'; - const slideUpSettings: PositionAnimationSettings = { + + const desktopSlideIn: PositionAnimationSettings = { timingFunction: 'ease-in', top: { from: slideAmount, to: '0px', }, + position: 'relative', }; - const slideDownSettings: PositionAnimationSettings = { + const desktopSlideOut: PositionAnimationSettings = { timingFunction: 'cubic-bezier(0.25, 0.1, 0.25, 1)', top: { from: '0px', to: slideAmount, }, + position: 'relative', + }; + + const mobileSlideIn: PositionAnimationSettings = { + duration: '0.5s', + timingFunction: 'ease-in', + top: { from: '-120px', to: '0px' }, + position: 'fixed', + }; + const moblieSlideOut: PositionAnimationSettings = { + duration: '0.5s', + timingFunction: 'ease-in', + top: { from: '0px', to: '-120px' }, + position: 'fixed', + }; + + const slideUpSettings: ScreenSpecification<PositionAnimationSettings> = { + default: desktopSlideIn, + sm: mobileSlideIn, }; + const slideOutSettings: ScreenSpecification<PositionAnimationSettings> = { + default: desktopSlideOut, + sm: moblieSlideOut, + }; + return ( <SlideAnimation - position="relative" slideInSettings={slideUpSettings} - slideOutSettings={slideDownSettings} + slideOutSettings={slideOutSettings} + zIndex={{ sm: zIndex.errorPopup, default: zIndex.errorPopBehind }} animationState={props.animationState} > <Error icon={props.icon} message={props.message} /> diff --git a/packages/instant/src/components/sliding_panel.tsx b/packages/instant/src/components/sliding_panel.tsx index 5e8a73663..99db58e68 100644 --- a/packages/instant/src/components/sliding_panel.tsx +++ b/packages/instant/src/components/sliding_panel.tsx @@ -53,6 +53,7 @@ export const SlidingPanel: React.StatelessComponent<SlidingPanelProps> = props = from: slideAmount, to: '0px', }, + position: 'absolute', }; const slideDownSettings: PositionAnimationSettings = { duration: '0.3s', @@ -61,10 +62,10 @@ export const SlidingPanel: React.StatelessComponent<SlidingPanelProps> = props = from: '0px', to: slideAmount, }, + position: 'absolute', }; return ( <SlideAnimation - position="absolute" slideInSettings={slideUpSettings} slideOutSettings={slideDownSettings} animationState={animationState} diff --git a/packages/instant/src/components/ui/container.tsx b/packages/instant/src/components/ui/container.tsx index 172d384b4..8aa5db9e5 100644 --- a/packages/instant/src/components/ui/container.tsx +++ b/packages/instant/src/components/ui/container.tsx @@ -67,9 +67,9 @@ export const Container = ${props => cssRuleIfExists(props, 'cursor')} ${props => cssRuleIfExists(props, 'overflow')} ${props => (props.hasBoxShadow ? `box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.1)` : '')}; - ${props => props.display && stylesForMedia('display', props.display)} - ${props => (props.width ? stylesForMedia('width', props.width) : '')} - ${props => (props.height ? stylesForMedia('height', props.height) : '')} + ${props => props.display && stylesForMedia<string>('display', props.display)} + ${props => props.width && stylesForMedia<string>('width', props.width)} + ${props => props.height && stylesForMedia<string>('height', props.height)} background-color: ${props => (props.backgroundColor ? props.theme[props.backgroundColor] : 'none')}; border-color: ${props => (props.borderColor ? props.theme[props.borderColor] : 'none')}; &:hover { diff --git a/packages/instant/src/components/zero_ex_instant_container.tsx b/packages/instant/src/components/zero_ex_instant_container.tsx index 2d3a1f4a8..5748e064e 100644 --- a/packages/instant/src/components/zero_ex_instant_container.tsx +++ b/packages/instant/src/components/zero_ex_instant_container.tsx @@ -33,7 +33,7 @@ export class ZeroExInstantContainer extends React.Component<ZeroExInstantContain height={{ default: 'auto', sm: '100%' }} position="relative" > - <Container zIndex={zIndex.errorPopup} position="relative"> + <Container position="relative"> <LatestError /> </Container> <Container diff --git a/packages/instant/src/components/zero_ex_instant_provider.tsx b/packages/instant/src/components/zero_ex_instant_provider.tsx index 1fb5cf64f..58e78c522 100644 --- a/packages/instant/src/components/zero_ex_instant_provider.tsx +++ b/packages/instant/src/components/zero_ex_instant_provider.tsx @@ -92,12 +92,12 @@ export class ZeroExInstantProvider extends React.Component<ZeroExInstantProvider // tslint:disable-next-line:no-floating-promises asyncData.fetchAvailableAssetDatasAndDispatchToStore(this._store); } - + // tslint:disable-next-line:no-floating-promises + asyncData.fetchCurrentBuyQuoteAndDispatchToStore(this._store); // warm up the gas price estimator cache just in case we can't // grab the gas price estimate when submitting the transaction // tslint:disable-next-line:no-floating-promises gasPriceEstimator.getGasInfoAsync(); - // tslint:disable-next-line:no-floating-promises this._flashErrorIfWrongNetwork(); } diff --git a/packages/instant/src/containers/selected_erc20_asset_amount_input.ts b/packages/instant/src/containers/selected_erc20_asset_amount_input.ts index 784eb4bd0..c550aef04 100644 --- a/packages/instant/src/containers/selected_erc20_asset_amount_input.ts +++ b/packages/instant/src/containers/selected_erc20_asset_amount_input.ts @@ -1,20 +1,17 @@ -import { AssetBuyer, AssetBuyerError, BuyQuote } from '@0x/asset-buyer'; +import { AssetBuyer } 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'; import { Dispatch } from 'redux'; -import { oc } from 'ts-optchain'; import { ERC20AssetAmountInput } from '../components/erc20_asset_amount_input'; import { Action, actions } from '../redux/actions'; import { State } from '../redux/reducer'; import { ColorOption } from '../style/theme'; import { AffiliateInfo, ERC20Asset, OrderProcessState } from '../types'; -import { assetUtils } from '../util/asset'; -import { errorFlasher } from '../util/error_flasher'; +import { buyQuoteUpdater } from '../util/buy_quote_updater'; export interface SelectedERC20AssetAmountInputProps { fontColor?: ColorOption; @@ -70,52 +67,9 @@ const mapStateToProps = (state: State, _ownProps: SelectedERC20AssetAmountInputP }; }; -const updateBuyQuoteAsync = async ( - assetBuyer: AssetBuyer, - dispatch: Dispatch<Action>, - asset: ERC20Asset, - assetAmount: BigNumber, - affiliateInfo?: AffiliateInfo, -): Promise<void> => { - // get a new buy quote. - const baseUnitValue = Web3Wrapper.toBaseUnitAmount(assetAmount, asset.metaData.decimals); - - // mark quote as pending - dispatch(actions.setQuoteRequestStatePending()); - - const feePercentage = oc(affiliateInfo).feePercentage(); - let newBuyQuote: BuyQuote | undefined; - try { - newBuyQuote = await assetBuyer.getBuyQuoteAsync(asset.assetData, baseUnitValue, { feePercentage }); - } catch (error) { - dispatch(actions.setQuoteRequestStateFailure()); - let errorMessage; - if (error.message === AssetBuyerError.InsufficientAssetLiquidity) { - const assetName = assetUtils.bestNameForAsset(asset, 'of this asset'); - errorMessage = `Not enough ${assetName} available`; - } else if (error.message === AssetBuyerError.InsufficientZrxLiquidity) { - errorMessage = 'Not enough ZRX available'; - } else if ( - error.message === AssetBuyerError.StandardRelayerApiError || - error.message.startsWith(AssetBuyerError.AssetUnavailable) - ) { - const assetName = assetUtils.bestNameForAsset(asset, 'This asset'); - errorMessage = `${assetName} is currently unavailable`; - } - if (!_.isUndefined(errorMessage)) { - errorFlasher.flashNewErrorMessage(dispatch, errorMessage); - } else { - throw error; - } - return; - } - // We have a successful new buy quote - errorFlasher.clearError(dispatch); - // invalidate the last buy quote. - dispatch(actions.updateLatestBuyQuote(newBuyQuote)); -}; - -const debouncedUpdateBuyQuoteAsync = _.debounce(updateBuyQuoteAsync, 200, { trailing: true }); +const debouncedUpdateBuyQuoteAsync = _.debounce(buyQuoteUpdater.updateBuyQuoteAsync.bind(buyQuoteUpdater), 200, { + trailing: true, +}); const mapDispatchToProps = ( dispatch: Dispatch<Action>, diff --git a/packages/instant/src/redux/async_data.ts b/packages/instant/src/redux/async_data.ts index c3d190af2..839a90778 100644 --- a/packages/instant/src/redux/async_data.ts +++ b/packages/instant/src/redux/async_data.ts @@ -1,7 +1,10 @@ +import { AssetProxyId } from '@0x/types'; import * as _ from 'lodash'; import { BIG_NUMBER_ZERO } from '../constants'; +import { ERC20Asset } from '../types'; import { assetUtils } from '../util/asset'; +import { buyQuoteUpdater } from '../util/buy_quote_updater'; import { coinbaseApi } from '../util/coinbase_api'; import { errorFlasher } from '../util/error_flasher'; @@ -33,4 +36,21 @@ export const asyncData = { store.dispatch(actions.setAvailableAssets([])); } }, + fetchCurrentBuyQuoteAndDispatchToStore: async (store: Store) => { + const { providerState, selectedAsset, selectedAssetAmount, affiliateInfo } = store.getState(); + const assetBuyer = providerState.assetBuyer; + if ( + !_.isUndefined(selectedAssetAmount) && + !_.isUndefined(selectedAsset) && + selectedAsset.metaData.assetProxyId === AssetProxyId.ERC20 + ) { + await buyQuoteUpdater.updateBuyQuoteAsync( + assetBuyer, + store.dispatch, + selectedAsset as ERC20Asset, + selectedAssetAmount, + affiliateInfo, + ); + } + }, }; diff --git a/packages/instant/src/style/media.ts b/packages/instant/src/style/media.ts index beabbac46..5e7aaba37 100644 --- a/packages/instant/src/style/media.ts +++ b/packages/instant/src/style/media.ts @@ -14,30 +14,38 @@ const generateMediaWrapper = (screenWidth: ScreenWidths) => (...args: any[]) => } `; -const media = { +export const media = { small: generateMediaWrapper(ScreenWidths.Sm), medium: generateMediaWrapper(ScreenWidths.Md), large: generateMediaWrapper(ScreenWidths.Lg), }; -export interface ScreenSpecifications { - default: string; - sm?: string; - md?: string; - lg?: string; +export interface ScreenSpecification<T> { + default: T; + sm?: T; + md?: T; + lg?: T; } -export type MediaChoice = string | ScreenSpecifications; -export const stylesForMedia = (cssPropertyName: string, choice: MediaChoice): InterpolationValue[] => { - if (typeof choice === 'string') { +export type OptionallyScreenSpecific<T> = T | ScreenSpecification<T>; +export type MediaChoice = OptionallyScreenSpecific<string>; +/** + * Given a css property name and a OptionallyScreenSpecific value, + * generates css properties with screen-specific viewport styling + */ +export function stylesForMedia<T extends string | number>( + cssPropertyName: string, + choice: OptionallyScreenSpecific<T>, +): InterpolationValue[] { + if (typeof choice === 'object') { return css` - ${cssPropertyName}: ${choice}; - `; - } - - return css` ${cssPropertyName}: ${choice.default}; ${choice.lg && media.large`${cssPropertyName}: ${choice.lg}`} ${choice.md && media.medium`${cssPropertyName}: ${choice.md}`} ${choice.sm && media.small`${cssPropertyName}: ${choice.sm}`} `; -}; + } else { + return css` + ${cssPropertyName}: ${choice}; + `; + } +} diff --git a/packages/instant/src/style/z_index.ts b/packages/instant/src/style/z_index.ts index 0eedcaf29..bd034182e 100644 --- a/packages/instant/src/style/z_index.ts +++ b/packages/instant/src/style/z_index.ts @@ -1,7 +1,8 @@ export const zIndex = { - errorPopup: 10, + errorPopBehind: 10, mainContainer: 20, dropdownItems: 30, panel: 40, + errorPopup: 50, overlayDefault: 100, }; diff --git a/packages/instant/src/util/buy_quote_updater.ts b/packages/instant/src/util/buy_quote_updater.ts new file mode 100644 index 000000000..e697d3ef7 --- /dev/null +++ b/packages/instant/src/util/buy_quote_updater.ts @@ -0,0 +1,56 @@ +import { AssetBuyer, AssetBuyerError, BuyQuote } from '@0x/asset-buyer'; +import { BigNumber } from '@0x/utils'; +import { Web3Wrapper } from '@0x/web3-wrapper'; +import * as _ from 'lodash'; +import { Dispatch } from 'redux'; +import { oc } from 'ts-optchain'; + +import { Action, actions } from '../redux/actions'; +import { AffiliateInfo, ERC20Asset } from '../types'; +import { assetUtils } from '../util/asset'; +import { errorFlasher } from '../util/error_flasher'; + +export const buyQuoteUpdater = { + updateBuyQuoteAsync: async ( + assetBuyer: AssetBuyer, + dispatch: Dispatch<Action>, + asset: ERC20Asset, + assetAmount: BigNumber, + affiliateInfo?: AffiliateInfo, + ): Promise<void> => { + // get a new buy quote. + const baseUnitValue = Web3Wrapper.toBaseUnitAmount(assetAmount, asset.metaData.decimals); + // mark quote as pending + dispatch(actions.setQuoteRequestStatePending()); + const feePercentage = oc(affiliateInfo).feePercentage(); + let newBuyQuote: BuyQuote | undefined; + try { + newBuyQuote = await assetBuyer.getBuyQuoteAsync(asset.assetData, baseUnitValue, { feePercentage }); + } catch (error) { + dispatch(actions.setQuoteRequestStateFailure()); + let errorMessage; + if (error.message === AssetBuyerError.InsufficientAssetLiquidity) { + const assetName = assetUtils.bestNameForAsset(asset, 'of this asset'); + errorMessage = `Not enough ${assetName} available`; + } else if (error.message === AssetBuyerError.InsufficientZrxLiquidity) { + errorMessage = 'Not enough ZRX available'; + } else if ( + error.message === AssetBuyerError.StandardRelayerApiError || + error.message.startsWith(AssetBuyerError.AssetUnavailable) + ) { + const assetName = assetUtils.bestNameForAsset(asset, 'This asset'); + errorMessage = `${assetName} is currently unavailable`; + } + if (!_.isUndefined(errorMessage)) { + errorFlasher.flashNewErrorMessage(dispatch, errorMessage); + } else { + throw error; + } + return; + } + // We have a successful new buy quote + errorFlasher.clearError(dispatch); + // invalidate the last buy quote. + dispatch(actions.updateLatestBuyQuote(newBuyQuote)); + }, +}; diff --git a/packages/sra-spec/src/json-schemas.ts b/packages/sra-spec/src/json-schemas.ts index 9eb32de59..f9342ca9e 100644 --- a/packages/sra-spec/src/json-schemas.ts +++ b/packages/sra-spec/src/json-schemas.ts @@ -2,6 +2,7 @@ import { schemas as jsonSchemas } from '@0x/json-schemas'; // Only include schemas we actually need const { + wholeNumberSchema, numberSchema, addressSchema, hexSchema, @@ -28,6 +29,7 @@ const { } = jsonSchemas; const usedSchemas = { + wholeNumberSchema, numberSchema, addressSchema, hexSchema, diff --git a/packages/utils/src/configured_bignumber.ts b/packages/utils/src/configured_bignumber.ts index 2b22b6938..34b57d303 100644 --- a/packages/utils/src/configured_bignumber.ts +++ b/packages/utils/src/configured_bignumber.ts @@ -11,4 +11,27 @@ BigNumber.config({ DECIMAL_PLACES: 78, }); +// Set a debug print function for NodeJS +// Upstream issue: https://github.com/MikeMcl/bignumber.js/issues/188 +import isNode = require('detect-node'); +if (isNode) { + // Dynamically load a NodeJS specific module. + // Typescript requires all imports to be global, so we need to use + // `const` here and disable the tslint warning. + // tslint:disable-next-line: no-var-requires + const util = require('util'); + + // Set a custom util.inspect function + // HACK: We add a function to the BigNumber class by assigning to the + // prototype. The function name is a symbol provided by Node. + (BigNumber.prototype as any)[util.inspect.custom] = function(): string { + // HACK: When executed, `this` will refer to the BigNumber instance. + // This is also why we need a function expression instead of an + // arrow function, as the latter does not have a `this`. + // Return the readable string representation + // tslint:disable-next-line: no-invalid-this + return this.toString(); + }; +} + export { BigNumber }; |