diff options
author | fragosti <francesco.agosti93@gmail.com> | 2018-10-25 03:56:11 +0800 |
---|---|---|
committer | fragosti <francesco.agosti93@gmail.com> | 2018-10-25 03:56:11 +0800 |
commit | 6da6540c038640abf75f703a294cb758c6defc47 (patch) | |
tree | bf320d263a86da9b0f75358305649f4d00b94265 /packages/instant/src | |
parent | f89b314a94f867fa905a1ce18eba9336ee4d1634 (diff) | |
parent | 06ba26a6d30565e7c6c4032528089d30ecc39fdd (diff) | |
download | dexon-sol-tools-6da6540c038640abf75f703a294cb758c6defc47.tar dexon-sol-tools-6da6540c038640abf75f703a294cb758c6defc47.tar.gz dexon-sol-tools-6da6540c038640abf75f703a294cb758c6defc47.tar.bz2 dexon-sol-tools-6da6540c038640abf75f703a294cb758c6defc47.tar.lz dexon-sol-tools-6da6540c038640abf75f703a294cb758c6defc47.tar.xz dexon-sol-tools-6da6540c038640abf75f703a294cb758c6defc47.tar.zst dexon-sol-tools-6da6540c038640abf75f703a294cb758c6defc47.zip |
Merge branch 'development' of https://github.com/0xProject/0x-monorepo into feature/instant/input-fees-rounding
Diffstat (limited to 'packages/instant/src')
24 files changed, 426 insertions, 167 deletions
diff --git a/packages/instant/src/components/amount_placeholder.tsx b/packages/instant/src/components/amount_placeholder.tsx new file mode 100644 index 000000000..6ef8f0ac3 --- /dev/null +++ b/packages/instant/src/components/amount_placeholder.tsx @@ -0,0 +1,32 @@ +import * as React from 'react'; + +import { ColorOption } from '../style/theme'; + +import { Pulse } from './animations/pulse'; + +import { Text } from './ui'; + +interface PlainPlaceholder { + color: ColorOption; +} +const PlainPlaceholder: React.StatelessComponent<PlainPlaceholder> = props => ( + <Text fontWeight="bold" fontColor={props.color}> + — + </Text> +); + +export interface AmountPlaceholderProps { + color: ColorOption; + isPulsating: boolean; +} +export const AmountPlaceholder: React.StatelessComponent<AmountPlaceholderProps> = props => { + if (props.isPulsating) { + return ( + <Pulse> + <PlainPlaceholder color={props.color} /> + </Pulse> + ); + } else { + return <PlainPlaceholder color={props.color} />; + } +}; diff --git a/packages/instant/src/components/animations/pulse.tsx b/packages/instant/src/components/animations/pulse.tsx new file mode 100644 index 000000000..01d6ea070 --- /dev/null +++ b/packages/instant/src/components/animations/pulse.tsx @@ -0,0 +1,15 @@ +import { keyframes, styled } from '../../style/theme'; + +const pulsingKeyframes = keyframes` + 0%, 100% { + opacity: 0.2; + } + 50% { + opacity: 100; + } +`; +export const Pulse = styled.div` + animation-name: ${pulsingKeyframes} + animation-duration: 2s; + animation-iteration-count: infinite; +`; diff --git a/packages/instant/src/components/buy_button.tsx b/packages/instant/src/components/buy_button.tsx index 3ef7c1f5c..f999d59a8 100644 --- a/packages/instant/src/components/buy_button.tsx +++ b/packages/instant/src/components/buy_button.tsx @@ -6,7 +6,7 @@ import { ColorOption } from '../style/theme'; import { util } from '../util/util'; import { web3Wrapper } from '../util/web3_wrapper'; -import { Button, Container, Text } from './ui'; +import { Button, Text } from './ui'; export interface BuyButtonProps { buyQuote?: BuyQuote; @@ -14,7 +14,6 @@ export interface BuyButtonProps { onClick: (buyQuote: BuyQuote) => void; onBuySuccess: (buyQuote: BuyQuote, txnHash: string) => void; onBuyFailure: (buyQuote: BuyQuote, tnxHash?: string) => void; - text: string; } export class BuyButton extends React.Component<BuyButtonProps> { @@ -24,15 +23,13 @@ export class BuyButton extends React.Component<BuyButtonProps> { onBuyFailure: util.boundNoop, }; public render(): React.ReactNode { - const shouldDisableButton = _.isUndefined(this.props.buyQuote); + const shouldDisableButton = _.isUndefined(this.props.buyQuote) || _.isUndefined(this.props.assetBuyer); return ( - <Container padding="20px" width="100%"> - <Button width="100%" onClick={this._handleClick} isDisabled={shouldDisableButton}> - <Text fontColor={ColorOption.white} fontWeight={600} fontSize="20px"> - {this.props.text} - </Text> - </Button> - </Container> + <Button width="100%" onClick={this._handleClick} isDisabled={shouldDisableButton}> + <Text fontColor={ColorOption.white} fontWeight={600} fontSize="20px"> + Buy + </Text> + </Button> ); } private readonly _handleClick = async () => { diff --git a/packages/instant/src/components/buy_order_state_button.tsx b/packages/instant/src/components/buy_order_state_button.tsx new file mode 100644 index 000000000..e7641e7e7 --- /dev/null +++ b/packages/instant/src/components/buy_order_state_button.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; + +import { SelectedAssetBuyButton } from '../containers/selected_asset_buy_button'; +import { SelectedAssetRetryButton } from '../containers/selected_asset_retry_button'; + +import { AsyncProcessState } from '../types'; + +import { SecondaryButton } from './secondary_button'; + +export interface BuyOrderStateButtonProps { + buyOrderState: AsyncProcessState; +} + +export const BuyOrderStateButton: React.StatelessComponent<BuyOrderStateButtonProps> = props => { + if (props.buyOrderState === AsyncProcessState.FAILURE) { + return <SelectedAssetRetryButton />; + } else if (props.buyOrderState === AsyncProcessState.SUCCESS) { + return <SecondaryButton text="Success" isDisabled={true} />; + } else if (props.buyOrderState === AsyncProcessState.PENDING) { + return <SecondaryButton text="Processing" isDisabled={true} />; + } + + return <SelectedAssetBuyButton />; +}; diff --git a/packages/instant/src/components/instant_heading.tsx b/packages/instant/src/components/instant_heading.tsx index c9f0ea95a..0efb58360 100644 --- a/packages/instant/src/components/instant_heading.tsx +++ b/packages/instant/src/components/instant_heading.tsx @@ -7,81 +7,109 @@ import { ColorOption } from '../style/theme'; import { AsyncProcessState } from '../types'; import { format } from '../util/format'; +import { AmountPlaceholder } from './amount_placeholder'; import { Container, Flex, Text } from './ui'; +import { Icon } from './ui/icon'; export interface InstantHeadingProps { selectedAssetAmount?: BigNumber; totalEthBaseAmount?: BigNumber; ethUsdPrice?: BigNumber; - quoteState: AsyncProcessState; + quoteRequestState: AsyncProcessState; + buyOrderState: AsyncProcessState; } -const Placeholder = () => ( - <Text fontWeight="bold" fontColor={ColorOption.white}> - — - </Text> -); -const displaytotalEthBaseAmount = ({ - selectedAssetAmount, - totalEthBaseAmount, -}: InstantHeadingProps): React.ReactNode => { - if (_.isUndefined(selectedAssetAmount)) { - return '0 ETH'; +const placeholderColor = ColorOption.white; +export class InstantHeading extends React.Component<InstantHeadingProps, {}> { + public render(): React.ReactNode { + const iconOrAmounts = this._renderIcon() || this._renderAmountsSection(); + return ( + <Container + backgroundColor={ColorOption.primaryColor} + padding="20px" + width="100%" + borderRadius="3px 3px 0px 0px" + > + <Container marginBottom="5px"> + <Text + letterSpacing="1px" + fontColor={ColorOption.white} + opacity={0.7} + fontWeight={500} + textTransform="uppercase" + fontSize="12px" + > + {this._renderTopText()} + </Text> + </Container> + <Flex direction="row" justify="space-between"> + <Flex height="60px"> + <SelectedAssetAmountInput startingFontSizePx={38} /> + </Flex> + <Flex direction="column" justify="space-between"> + {iconOrAmounts} + </Flex> + </Flex> + </Container> + ); } - return format.ethBaseAmount(totalEthBaseAmount, 4, <Placeholder />); -}; -const displayUsdAmount = ({ - totalEthBaseAmount, - selectedAssetAmount, - ethUsdPrice, -}: InstantHeadingProps): React.ReactNode => { - if (_.isUndefined(selectedAssetAmount)) { - return '$0.00'; + private _renderAmountsSection(): React.ReactNode { + return ( + <Container> + <Container marginBottom="5px">{this._placeholderOrAmount(this._ethAmount)}</Container> + <Container opacity={0.7}>{this._placeholderOrAmount(this._dollarAmount)}</Container> + </Container> + ); + } + + private _renderIcon(): React.ReactNode { + if (this.props.buyOrderState === AsyncProcessState.FAILURE) { + return <Icon icon={'failed'} width={34} height={34} color={ColorOption.white} />; + } + return undefined; } - return format.ethBaseAmountInUsd(totalEthBaseAmount, ethUsdPrice, 2, <Placeholder />); -}; -const loadingOrAmount = (quoteState: AsyncProcessState, amount: React.ReactNode): React.ReactNode => { - if (quoteState === AsyncProcessState.PENDING) { + private _renderTopText(): React.ReactNode { + if (this.props.buyOrderState === AsyncProcessState.FAILURE) { + return 'Order failed'; + } + + return 'I want to buy'; + } + + private _placeholderOrAmount(amountFunction: () => React.ReactNode): React.ReactNode { + if (this.props.quoteRequestState === AsyncProcessState.PENDING) { + return <AmountPlaceholder isPulsating={true} color={placeholderColor} />; + } + if (_.isUndefined(this.props.selectedAssetAmount)) { + return <AmountPlaceholder isPulsating={false} color={placeholderColor} />; + } + return amountFunction(); + } + + private readonly _ethAmount = (): React.ReactNode => { return ( - <Text fontWeight="bold" fontColor={ColorOption.white}> - …loading + <Text fontSize="16px" fontColor={ColorOption.white} fontWeight={500}> + {format.ethBaseAmount( + this.props.totalEthBaseAmount, + 4, + <AmountPlaceholder isPulsating={false} color={placeholderColor} />, + )} </Text> ); - } else { - return amount; - } -}; + }; -export const InstantHeading: React.StatelessComponent<InstantHeadingProps> = props => ( - <Container backgroundColor={ColorOption.primaryColor} padding="20px" width="100%" borderRadius="3px 3px 0px 0px"> - <Container marginBottom="5px"> - <Text - letterSpacing="1px" - fontColor={ColorOption.white} - opacity={0.7} - fontWeight={500} - textTransform="uppercase" - fontSize="12px" - > - I want to buy + private readonly _dollarAmount = (): React.ReactNode => { + return ( + <Text fontSize="16px" fontColor={ColorOption.white}> + {format.ethBaseAmountInUsd( + this.props.totalEthBaseAmount, + this.props.ethUsdPrice, + 2, + <AmountPlaceholder isPulsating={false} color={ColorOption.white} />, + )} </Text> - </Container> - <Flex direction="row" justify="space-between"> - <Flex height="65px"> - <SelectedAssetAmountInput startingFontSizePx={38} /> - </Flex> - <Flex direction="column" justify="space-between"> - <Container marginBottom="5px" whiteSpace="nowrap"> - <Text fontSize="16px" fontColor={ColorOption.white} fontWeight={500}> - {loadingOrAmount(props.quoteState, displaytotalEthBaseAmount(props))} - </Text> - </Container> - <Text fontSize="16px" fontColor={ColorOption.white} opacity={0.7}> - {loadingOrAmount(props.quoteState, displayUsdAmount(props))} - </Text> - </Flex> - </Flex> - </Container> -); + ); + }; +} diff --git a/packages/instant/src/components/order_details.tsx b/packages/instant/src/components/order_details.tsx index ad4a87714..704009d89 100644 --- a/packages/instant/src/components/order_details.tsx +++ b/packages/instant/src/components/order_details.tsx @@ -7,13 +7,14 @@ import { oc } from 'ts-optchain'; import { ColorOption } from '../style/theme'; import { format } from '../util/format'; +import { AmountPlaceholder } from './amount_placeholder'; import { Container, Flex, Text } from './ui'; export interface OrderDetailsProps { buyQuoteInfo?: BuyQuoteInfo; ethUsdPrice?: BigNumber; + isLoading: boolean; } - export class OrderDetails extends React.Component<OrderDetailsProps> { public render(): React.ReactNode { const { buyQuoteInfo, ethUsdPrice } = this.props; @@ -39,13 +40,20 @@ export class OrderDetails extends React.Component<OrderDetailsProps> { ethAmount={ethAssetPrice} ethUsdPrice={ethUsdPrice} isEthAmountInBaseUnits={false} + isLoading={this.props.isLoading} + /> + <EthAmountRow + rowLabel="Fee" + ethAmount={ethTokenFee} + ethUsdPrice={ethUsdPrice} + isLoading={this.props.isLoading} /> - <EthAmountRow rowLabel="Fee" ethAmount={ethTokenFee} ethUsdPrice={ethUsdPrice} /> <EthAmountRow rowLabel="Total Cost" ethAmount={totalEthAmount} ethUsdPrice={ethUsdPrice} shouldEmphasize={true} + isLoading={this.props.isLoading} /> </Container> ); @@ -58,43 +66,50 @@ export interface EthAmountRowProps { isEthAmountInBaseUnits?: boolean; ethUsdPrice?: BigNumber; shouldEmphasize?: boolean; + isLoading: boolean; } -export const EthAmountRow: React.StatelessComponent<EthAmountRowProps> = ({ - rowLabel, - ethAmount, - isEthAmountInBaseUnits, - ethUsdPrice, - shouldEmphasize, -}) => { - const fontWeight = shouldEmphasize ? 700 : 400; - const usdFormatter = isEthAmountInBaseUnits ? format.ethBaseAmountInUsd : format.ethUnitAmountInUsd; - const ethFormatter = isEthAmountInBaseUnits ? format.ethBaseAmount : format.ethUnitAmount; - const usdPriceSection = _.isUndefined(ethUsdPrice) ? null : ( - <Container marginRight="3px" display="inline-block"> - <Text fontColor={ColorOption.lightGrey}>({usdFormatter(ethAmount, ethUsdPrice)})</Text> - </Container> - ); - return ( - <Container padding="10px 0px" borderTop="1px dashed" borderColor={ColorOption.feintGrey}> - <Flex justify="space-between"> - <Text fontWeight={fontWeight} fontColor={ColorOption.grey}> - {rowLabel} - </Text> - <Container> - {usdPriceSection} +export class EthAmountRow extends React.Component<EthAmountRowProps> { + public static defaultProps = { + shouldEmphasize: false, + isEthAmountInBaseUnits: true, + }; + public render(): React.ReactNode { + const { rowLabel, ethAmount, isEthAmountInBaseUnits, shouldEmphasize, isLoading } = this.props; + + const fontWeight = shouldEmphasize ? 700 : 400; + const ethFormatter = isEthAmountInBaseUnits ? format.ethBaseAmount : format.ethUnitAmount; + return ( + <Container padding="10px 0px" borderTop="1px dashed" borderColor={ColorOption.feintGrey}> + <Flex justify="space-between"> <Text fontWeight={fontWeight} fontColor={ColorOption.grey}> - {ethFormatter(ethAmount)} + {rowLabel} </Text> - </Container> - </Flex> - </Container> - ); -}; - -EthAmountRow.defaultProps = { - shouldEmphasize: false, - isEthAmountInBaseUnits: true, -}; - -EthAmountRow.displayName = 'EthAmountRow'; + <Container> + {this._renderUsdSection()} + <Text fontWeight={fontWeight} fontColor={ColorOption.grey}> + {ethFormatter( + ethAmount, + 4, + <Container opacity={0.5}> + <AmountPlaceholder color={ColorOption.lightGrey} isPulsating={isLoading} /> + </Container>, + )} + </Text> + </Container> + </Flex> + </Container> + ); + } + private _renderUsdSection(): React.ReactNode { + const usdFormatter = this.props.isEthAmountInBaseUnits ? format.ethBaseAmountInUsd : format.ethUnitAmountInUsd; + const shouldHideUsdPriceSection = _.isUndefined(this.props.ethUsdPrice) || _.isUndefined(this.props.ethAmount); + return shouldHideUsdPriceSection ? null : ( + <Container marginRight="3px" display="inline-block"> + <Text fontColor={ColorOption.lightGrey}> + ({usdFormatter(this.props.ethAmount, this.props.ethUsdPrice)}) + </Text> + </Container> + ); + } +} diff --git a/packages/instant/src/components/retry_button.tsx b/packages/instant/src/components/retry_button.tsx new file mode 100644 index 000000000..28547ce54 --- /dev/null +++ b/packages/instant/src/components/retry_button.tsx @@ -0,0 +1,11 @@ +import * as React from 'react'; + +import { SecondaryButton } from './secondary_button'; + +export interface RetryButtonProps { + onClick: () => void; +} + +export const RetryButton: React.StatelessComponent<RetryButtonProps> = props => { + return <SecondaryButton text="Try Again" onClick={props.onClick} />; +}; diff --git a/packages/instant/src/components/secondary_button.tsx b/packages/instant/src/components/secondary_button.tsx new file mode 100644 index 000000000..e073f6061 --- /dev/null +++ b/packages/instant/src/components/secondary_button.tsx @@ -0,0 +1,28 @@ +import * as _ from 'lodash'; +import * as React from 'react'; + +import { ColorOption } from '../style/theme'; + +import { Button, ButtonProps } from './ui/button'; +import { Text } from './ui/text'; + +export interface SecondaryButtonProps extends ButtonProps { + text: string; +} + +export const SecondaryButton: React.StatelessComponent<SecondaryButtonProps> = props => { + const buttonProps = _.omit(props, 'text'); + return ( + <Button + backgroundColor={ColorOption.white} + borderColor={ColorOption.lightGrey} + width="100%" + onClick={props.onClick} + {...buttonProps} + > + <Text fontColor={ColorOption.primaryColor} fontWeight={600} fontSize="16px"> + {props.text} + </Text> + </Button> + ); +}; diff --git a/packages/instant/src/components/ui/container.tsx b/packages/instant/src/components/ui/container.tsx index e44cfff1e..7077d0aa3 100644 --- a/packages/instant/src/components/ui/container.tsx +++ b/packages/instant/src/components/ui/container.tsx @@ -27,6 +27,7 @@ export interface ContainerProps { hasBoxShadow?: boolean; zIndex?: number; whiteSpace?: string; + opacity?: number; } export const Container = styled<ContainerProps, 'div'>('div')` @@ -52,6 +53,7 @@ export const Container = styled<ContainerProps, 'div'>('div')` ${props => cssRuleIfExists(props, 'border-bottom')} ${props => cssRuleIfExists(props, 'z-index')} ${props => cssRuleIfExists(props, 'white-space')} + ${props => cssRuleIfExists(props, 'opacity')} ${props => (props.hasBoxShadow ? `box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.1)` : '')}; background-color: ${props => (props.backgroundColor ? props.theme[props.backgroundColor] : 'none')}; border-color: ${props => (props.borderColor ? props.theme[props.borderColor] : 'none')}; diff --git a/packages/instant/src/components/ui/icon.tsx b/packages/instant/src/components/ui/icon.tsx new file mode 100644 index 000000000..7373c3acd --- /dev/null +++ b/packages/instant/src/components/ui/icon.tsx @@ -0,0 +1,58 @@ +import * as React from 'react'; + +import { ColorOption } from '../../style/theme'; + +type svgRule = 'evenodd' | 'nonzero' | 'inherit'; +interface IconInfo { + viewBox: string; + fillRule?: svgRule; + clipRule?: svgRule; + path: string; +} +interface IconInfoMapping { + failed: IconInfo; + success: IconInfo; +} +const ICONS: IconInfoMapping = { + failed: { + viewBox: '0 0 34 34', + fillRule: 'evenodd', + clipRule: 'evenodd', + path: + 'M6.65771 26.4362C9.21777 29.2406 12.9033 31 17 31C24.7319 31 31 24.7319 31 17C31 14.4468 30.3164 12.0531 29.1226 9.99219L6.65771 26.4362ZM4.88281 24.0173C3.68555 21.9542 3 19.5571 3 17C3 9.26807 9.26807 3 17 3C21.1006 3 24.7891 4.76294 27.3496 7.57214L4.88281 24.0173ZM0 17C0 26.3888 7.61133 34 17 34C26.3887 34 34 26.3888 34 17C34 7.61121 26.3887 0 17 0C7.61133 0 0 7.61121 0 17Z', + }, + success: { + viewBox: '0 0 34 34', + fillRule: 'evenodd', + clipRule: 'evenodd', + path: + 'M17 34C26.3887 34 34 26.3888 34 17C34 7.61121 26.3887 0 17 0C7.61133 0 0 7.61121 0 17C0 26.3888 7.61133 34 17 34ZM25.7539 13.0977C26.2969 12.4718 26.2295 11.5244 25.6035 10.9817C24.9775 10.439 24.0303 10.5063 23.4878 11.1323L15.731 20.0771L12.3936 16.7438C11.8071 16.1583 10.8574 16.1589 10.272 16.7451C9.68652 17.3313 9.6875 18.281 10.2734 18.8665L14.75 23.3373L15.8887 24.4746L16.9434 23.2587L25.7539 13.0977Z', + }, +}; + +export interface IconProps { + width: number; + height: number; + color: ColorOption; + icon: keyof IconInfoMapping; +} +export const Icon: React.SFC<IconProps> = props => { + const iconInfo = ICONS[props.icon]; + + return ( + <svg + width={props.width} + height={props.height} + viewBox={iconInfo.viewBox} + fill="none" + xmlns="http://www.w3.org/2000/svg" + > + <path + d={iconInfo.path} + fill={props.color} + fillRule={iconInfo.fillRule || 'nonzero'} + clipRule={iconInfo.clipRule || 'nonzero'} + /> + </svg> + ); +}; diff --git a/packages/instant/src/components/zero_ex_instant.tsx b/packages/instant/src/components/zero_ex_instant.tsx index 142e74dae..ffa5a8250 100644 --- a/packages/instant/src/components/zero_ex_instant.tsx +++ b/packages/instant/src/components/zero_ex_instant.tsx @@ -31,7 +31,7 @@ export interface ZeroExInstantOptionalProps { } export class ZeroExInstant extends React.Component<ZeroExInstantProps> { - public store: Store; + private readonly _store: Store; private static _mergeInitialStateWithProps(props: ZeroExInstantProps, state: State = INITIAL_STATE): State { // Create merged object such that properties in props override default settings const optionalPropsWithDefaults: ZeroExInstantOptionalProps = { @@ -58,14 +58,14 @@ export class ZeroExInstant extends React.Component<ZeroExInstantProps> { } constructor(props: ZeroExInstantProps) { super(props); - this.store = store.create(ZeroExInstant._mergeInitialStateWithProps(this.props, INITIAL_STATE)); + this._store = store.create(ZeroExInstant._mergeInitialStateWithProps(this.props, INITIAL_STATE)); // tslint:disable-next-line:no-floating-promises - asyncData.fetchAndDispatchToStore(this.store); + asyncData.fetchAndDispatchToStore(this._store); } public render(): React.ReactNode { return ( - <Provider store={this.store}> + <Provider store={this._store}> <SelectedAssetThemeProvider> <ZeroExInstantContainer /> </SelectedAssetThemeProvider> diff --git a/packages/instant/src/components/zero_ex_instant_container.tsx b/packages/instant/src/components/zero_ex_instant_container.tsx index cf918d890..1d17ed12a 100644 --- a/packages/instant/src/components/zero_ex_instant_container.tsx +++ b/packages/instant/src/components/zero_ex_instant_container.tsx @@ -2,7 +2,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 { SelectedAssetBuyOrderStateButton } from '../containers/selected_asset_buy_order_state_button'; import { SelectedAssetInstantHeading } from '../containers/selected_asset_instant_heading'; import { ColorOption } from '../style/theme'; @@ -26,7 +26,9 @@ export const ZeroExInstantContainer: React.StatelessComponent<ZeroExInstantConta <Flex direction="column" justify="flex-start"> <SelectedAssetInstantHeading /> <LatestBuyQuoteOrderDetails /> - <SelectedAssetBuyButton /> + <Container padding="20px" width="100%"> + <SelectedAssetBuyOrderStateButton /> + </Container> </Flex> </Container> </Container> diff --git a/packages/instant/src/constants.ts b/packages/instant/src/constants.ts index 7f4c5a058..31491c80a 100644 --- a/packages/instant/src/constants.ts +++ b/packages/instant/src/constants.ts @@ -1,3 +1,4 @@ import { BigNumber } from '@0x/utils'; export const BIG_NUMBER_ZERO = new BigNumber(0); export const ethDecimals = 18; +export const DEFAULT_ZERO_EX_CONTAINER_SELECTOR = '#zeroExInstantContainer'; 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 597bf3088..092aaaf20 100644 --- a/packages/instant/src/containers/latest_buy_quote_order_details.ts +++ b/packages/instant/src/containers/latest_buy_quote_order_details.ts @@ -8,18 +8,21 @@ import { oc } from 'ts-optchain'; import { State } from '../redux/reducer'; import { OrderDetails } from '../components/order_details'; +import { AsyncProcessState } from '../types'; export interface LatestBuyQuoteOrderDetailsProps {} interface ConnectedState { buyQuoteInfo?: BuyQuoteInfo; ethUsdPrice?: BigNumber; + isLoading: boolean; } const mapStateToProps = (state: State, _ownProps: LatestBuyQuoteOrderDetailsProps): ConnectedState => ({ // use the worst case quote info buyQuoteInfo: oc(state).latestBuyQuote.worstCaseQuoteInfo(), ethUsdPrice: state.ethUsdPrice, + isLoading: state.quoteRequestState === AsyncProcessState.PENDING, }); export const LatestBuyQuoteOrderDetails: React.ComponentClass<LatestBuyQuoteOrderDetailsProps> = connect( diff --git a/packages/instant/src/containers/latest_error.tsx b/packages/instant/src/containers/latest_error.tsx index 1d02cab23..b75ec00aa 100644 --- a/packages/instant/src/containers/latest_error.tsx +++ b/packages/instant/src/containers/latest_error.tsx @@ -3,8 +3,8 @@ 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 { State } from '../redux/reducer'; +import { Asset, DisplayStatus } from '../types'; import { errorUtil } from '../util/error'; export interface LatestErrorComponentProps { @@ -30,7 +30,7 @@ export interface LatestErrorProps {} const mapStateToProps = (state: State, _ownProps: LatestErrorProps): ConnectedState => ({ asset: state.selectedAsset, latestError: state.latestError, - slidingDirection: state.latestErrorDisplay === LatestErrorDisplay.Present ? 'up' : 'down', + slidingDirection: state.latestErrorDisplay === DisplayStatus.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 1b5f3cd22..e1438e920 100644 --- a/packages/instant/src/containers/selected_asset_amount_input.ts +++ b/packages/instant/src/containers/selected_asset_amount_input.ts @@ -62,13 +62,13 @@ const updateBuyQuoteAsync = async ( const baseUnitValue = Web3Wrapper.toBaseUnitAmount(assetAmount, asset.metaData.decimals); // mark quote as pending - dispatch(actions.updateBuyQuoteStatePending()); + dispatch(actions.setQuoteRequestStatePending()); let newBuyQuote: BuyQuote | undefined; try { newBuyQuote = await assetBuyer.getBuyQuoteAsync(asset.assetData, baseUnitValue); } catch (error) { - dispatch(actions.updateBuyQuoteStateFailure()); + dispatch(actions.setQuoteRequestStateFailure()); errorUtil.errorFlasher.flashNewError(dispatch, error); return; } @@ -90,11 +90,11 @@ const mapDispatchToProps = ( // invalidate the last buy quote. dispatch(actions.updateLatestBuyQuote(undefined)); // reset our buy state - dispatch(actions.updatebuyOrderState(AsyncProcessState.NONE)); + 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()); + dispatch(actions.setQuoteRequestStatePending()); // 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 99f971321..208bb2582 100644 --- a/packages/instant/src/containers/selected_asset_buy_button.ts +++ b/packages/instant/src/containers/selected_asset_buy_button.ts @@ -14,7 +14,6 @@ export interface SelectedAssetBuyButtonProps {} interface ConnectedState { assetBuyer?: AssetBuyer; - text: string; buyQuote?: BuyQuote; } @@ -24,31 +23,15 @@ interface ConnectedDispatch { onBuyFailure: (buyQuote: BuyQuote) => void; } -const textForState = (state: AsyncProcessState): string => { - switch (state) { - case AsyncProcessState.NONE: - return 'Buy'; - case AsyncProcessState.PENDING: - return '...Loading'; - case AsyncProcessState.SUCCESS: - return 'Success!'; - case AsyncProcessState.FAILURE: - return 'Failed'; - default: - return 'Buy'; - } -}; - const mapStateToProps = (state: State, _ownProps: SelectedAssetBuyButtonProps): ConnectedState => ({ assetBuyer: state.assetBuyer, - text: textForState(state.buyOrderState), buyQuote: state.latestBuyQuote, }); const mapDispatchToProps = (dispatch: Dispatch<Action>, ownProps: SelectedAssetBuyButtonProps): ConnectedDispatch => ({ - onClick: buyQuote => dispatch(actions.updatebuyOrderState(AsyncProcessState.PENDING)), - onBuySuccess: buyQuote => dispatch(actions.updatebuyOrderState(AsyncProcessState.SUCCESS)), - onBuyFailure: buyQuote => dispatch(actions.updatebuyOrderState(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_buy_order_state_button.tsx b/packages/instant/src/containers/selected_asset_buy_order_state_button.tsx new file mode 100644 index 000000000..3b7fc0054 --- /dev/null +++ b/packages/instant/src/containers/selected_asset_buy_order_state_button.tsx @@ -0,0 +1,20 @@ +import * as _ from 'lodash'; +import * as React from 'react'; +import { connect } from 'react-redux'; + +import { State } from '../redux/reducer'; +import { AsyncProcessState } from '../types'; + +import { BuyOrderStateButton } from '../components/buy_order_state_button'; + +interface ConnectedState { + buyOrderState: AsyncProcessState; +} +export interface SelectedAssetButtonProps {} +const mapStateToProps = (state: State, _ownProps: SelectedAssetButtonProps): ConnectedState => ({ + buyOrderState: state.buyOrderState, +}); + +export const SelectedAssetBuyOrderStateButton: React.ComponentClass<SelectedAssetButtonProps> = connect( + mapStateToProps, +)(BuyOrderStateButton); diff --git a/packages/instant/src/containers/selected_asset_instant_heading.ts b/packages/instant/src/containers/selected_asset_instant_heading.ts index 0509db5da..24efed32e 100644 --- a/packages/instant/src/containers/selected_asset_instant_heading.ts +++ b/packages/instant/src/containers/selected_asset_instant_heading.ts @@ -15,14 +15,16 @@ interface ConnectedState { selectedAssetAmount?: BigNumber; totalEthBaseAmount?: BigNumber; ethUsdPrice?: BigNumber; - quoteState: AsyncProcessState; + quoteRequestState: AsyncProcessState; + buyOrderState: AsyncProcessState; } const mapStateToProps = (state: State, _ownProps: InstantHeadingProps): ConnectedState => ({ selectedAssetAmount: state.selectedAssetAmount, totalEthBaseAmount: oc(state).latestBuyQuote.worstCaseQuoteInfo.totalEthAmount(), ethUsdPrice: state.ethUsdPrice, - quoteState: state.quoteState, + quoteRequestState: state.quoteRequestState, + buyOrderState: state.buyOrderState, }); export const SelectedAssetInstantHeading: React.ComponentClass<InstantHeadingProps> = connect(mapStateToProps)( diff --git a/packages/instant/src/containers/selected_asset_retry_button.tsx b/packages/instant/src/containers/selected_asset_retry_button.tsx new file mode 100644 index 000000000..b2b140be6 --- /dev/null +++ b/packages/instant/src/containers/selected_asset_retry_button.tsx @@ -0,0 +1,26 @@ +import * as _ from 'lodash'; +import * as React from 'react'; +import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; + +import { Action, actions } from '../redux/actions'; + +import { RetryButton } from '../components/retry_button'; + +export interface SelectedAssetRetryButtonProps {} + +interface ConnectedDispatch { + onClick: () => void; +} + +const mapDispatchToProps = ( + dispatch: Dispatch<Action>, + _ownProps: SelectedAssetRetryButtonProps, +): ConnectedDispatch => ({ + onClick: () => dispatch(actions.resetAmount()), +}); + +export const SelectedAssetRetryButton: React.ComponentClass<SelectedAssetRetryButtonProps> = connect( + undefined, + mapDispatchToProps, +)(RetryButton); diff --git a/packages/instant/src/index.umd.ts b/packages/instant/src/index.umd.ts index fe78ce401..f648b37f2 100644 --- a/packages/instant/src/index.umd.ts +++ b/packages/instant/src/index.umd.ts @@ -1,8 +1,9 @@ import * as React from 'react'; import * as ReactDOM from 'react-dom'; +import { DEFAULT_ZERO_EX_CONTAINER_SELECTOR } from './constants'; import { ZeroExInstant, ZeroExInstantProps } from './index'; -export const render = (props: ZeroExInstantProps, selector: string = '#zeroExInstantContainer') => { +export const render = (props: ZeroExInstantProps, selector: string = DEFAULT_ZERO_EX_CONTAINER_SELECTOR) => { ReactDOM.render(React.createElement(ZeroExInstant, props), document.querySelector(selector)); }; diff --git a/packages/instant/src/redux/actions.ts b/packages/instant/src/redux/actions.ts index bc75ce66c..f5946d707 100644 --- a/packages/instant/src/redux/actions.ts +++ b/packages/instant/src/redux/actions.ts @@ -26,23 +26,25 @@ 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_QUOTE_REQUEST_STATE_PENDING = 'SET_QUOTE_REQUEST_STATE_PENDING', + SET_QUOTE_REQUEST_STATE_FAILURE = 'SET_QUOTE_REQUEST_STATE_FAILURE', SET_ERROR = 'SET_ERROR', HIDE_ERROR = 'HIDE_ERROR', CLEAR_ERROR = 'CLEAR_ERROR', + RESET_AMOUNT = 'RESET_AMOUNT', } export const actions = { updateEthUsdPrice: (price?: BigNumber) => createAction(ActionTypes.UPDATE_ETH_USD_PRICE, price), updateSelectedAssetAmount: (amount?: BigNumber) => createAction(ActionTypes.UPDATE_SELECTED_ASSET_AMOUNT, amount), - updatebuyOrderState: (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), + setQuoteRequestStatePending: () => createAction(ActionTypes.SET_QUOTE_REQUEST_STATE_PENDING), + setQuoteRequestStateFailure: () => createAction(ActionTypes.SET_QUOTE_REQUEST_STATE_FAILURE), setError: (error?: any) => createAction(ActionTypes.SET_ERROR, error), hideError: () => createAction(ActionTypes.HIDE_ERROR), clearError: () => createAction(ActionTypes.CLEAR_ERROR), + resetAmount: () => createAction(ActionTypes.RESET_AMOUNT), }; diff --git a/packages/instant/src/redux/reducer.ts b/packages/instant/src/redux/reducer.ts index e9e0a85e0..ddcb144b1 100644 --- a/packages/instant/src/redux/reducer.ts +++ b/packages/instant/src/redux/reducer.ts @@ -4,15 +4,11 @@ import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; import { assetMetaDataMap } from '../data/asset_meta_data_map'; -import { Asset, AssetMetaData, AsyncProcessState, Network } from '../types'; +import { Asset, AssetMetaData, AsyncProcessState, DisplayStatus, Network } from '../types'; import { assetUtils } from '../util/asset'; import { Action, ActionTypes } from './actions'; -export enum LatestErrorDisplay { - Present, - Hidden, -} export interface State { network: Network; assetBuyer?: AssetBuyer; @@ -22,9 +18,9 @@ export interface State { buyOrderState: AsyncProcessState; ethUsdPrice?: BigNumber; latestBuyQuote?: BuyQuote; - quoteState: AsyncProcessState; + quoteRequestState: AsyncProcessState; latestError?: any; - latestErrorDisplay: LatestErrorDisplay; + latestErrorDisplay: DisplayStatus; } export const INITIAL_STATE: State = { @@ -35,8 +31,8 @@ export const INITIAL_STATE: State = { ethUsdPrice: undefined, latestBuyQuote: undefined, latestError: undefined, - latestErrorDisplay: LatestErrorDisplay.Hidden, - quoteState: AsyncProcessState.NONE, + latestErrorDisplay: DisplayStatus.Hidden, + quoteRequestState: AsyncProcessState.NONE, }; export const reducer = (state: State = INITIAL_STATE, action: Action): State => { @@ -55,19 +51,19 @@ export const reducer = (state: State = INITIAL_STATE, action: Action): State => return { ...state, latestBuyQuote: action.data, - quoteState: AsyncProcessState.SUCCESS, + quoteRequestState: AsyncProcessState.SUCCESS, }; - case ActionTypes.UPDATE_BUY_QUOTE_STATE_PENDING: + case ActionTypes.SET_QUOTE_REQUEST_STATE_PENDING: return { ...state, latestBuyQuote: undefined, - quoteState: AsyncProcessState.PENDING, + quoteRequestState: AsyncProcessState.PENDING, }; - case ActionTypes.UPDATE_BUY_QUOTE_STATE_FAILURE: + case ActionTypes.SET_QUOTE_REQUEST_STATE_FAILURE: return { ...state, latestBuyQuote: undefined, - quoteState: AsyncProcessState.FAILURE, + quoteRequestState: AsyncProcessState.FAILURE, }; case ActionTypes.UPDATE_SELECTED_ASSET_BUY_STATE: return { @@ -78,18 +74,18 @@ export const reducer = (state: State = INITIAL_STATE, action: Action): State => return { ...state, latestError: action.data, - latestErrorDisplay: LatestErrorDisplay.Present, + latestErrorDisplay: DisplayStatus.Present, }; case ActionTypes.HIDE_ERROR: return { ...state, - latestErrorDisplay: LatestErrorDisplay.Hidden, + latestErrorDisplay: DisplayStatus.Hidden, }; case ActionTypes.CLEAR_ERROR: return { ...state, latestError: undefined, - latestErrorDisplay: LatestErrorDisplay.Hidden, + latestErrorDisplay: DisplayStatus.Hidden, }; case ActionTypes.UPDATE_SELECTED_ASSET: const newSelectedAssetData = action.data; @@ -105,6 +101,14 @@ export const reducer = (state: State = INITIAL_STATE, action: Action): State => ...state, selectedAsset: newSelectedAsset, }; + case ActionTypes.RESET_AMOUNT: + return { + ...state, + latestBuyQuote: undefined, + quoteRequestState: AsyncProcessState.NONE, + buyOrderState: AsyncProcessState.NONE, + selectedAssetAmount: undefined, + }; default: return state; } diff --git a/packages/instant/src/types.ts b/packages/instant/src/types.ts index 0454bceea..c340623ad 100644 --- a/packages/instant/src/types.ts +++ b/packages/instant/src/types.ts @@ -7,6 +7,10 @@ export enum AsyncProcessState { SUCCESS = 'Success', FAILURE = 'Failure', } +export enum DisplayStatus { + Present, + Hidden, +} export type FunctionType = (...args: any[]) => any; export type ActionCreatorsMapObject = ObjectMap<FunctionType>; @@ -37,6 +41,7 @@ export interface ERC721Asset { assetData: string; metaData: ERC721AssetMetaData; } + export interface Asset { assetData: string; metaData: AssetMetaData; |