diff options
Diffstat (limited to 'packages/instant/src')
33 files changed, 455 insertions, 95 deletions
diff --git a/packages/instant/src/components/animations/position_animation.tsx b/packages/instant/src/components/animations/position_animation.tsx index 576d29c07..8b3b294b7 100644 --- a/packages/instant/src/components/animations/position_animation.tsx +++ b/packages/instant/src/components/animations/position_animation.tsx @@ -77,6 +77,7 @@ const generatePositionAnimationCss = (positionSettings: PositionAnimationSetting export interface PositionAnimationProps { positionSettings: OptionallyScreenSpecific<PositionAnimationSettings>; zIndex?: OptionallyScreenSpecific<number>; + height?: string; } const defaultAnimation = (positionSettings: OptionallyScreenSpecific<PositionAnimationSettings>) => { @@ -104,5 +105,6 @@ export const PositionAnimation = ${props => animationForSize(props.positionSettings, 'sm', media.small)} ${props => animationForSize(props.positionSettings, 'md', media.medium)} ${props => animationForSize(props.positionSettings, 'lg', media.large)} + ${props => (props.height ? `height: ${props.height};` : '')} } `; diff --git a/packages/instant/src/components/animations/slide_animation.tsx b/packages/instant/src/components/animations/slide_animation.tsx index 122229dee..9adb1c674 100644 --- a/packages/instant/src/components/animations/slide_animation.tsx +++ b/packages/instant/src/components/animations/slide_animation.tsx @@ -10,6 +10,7 @@ export interface SlideAnimationProps { slideInSettings: OptionallyScreenSpecific<PositionAnimationSettings>; slideOutSettings: OptionallyScreenSpecific<PositionAnimationSettings>; zIndex?: OptionallyScreenSpecific<number>; + height?: string; } export const SlideAnimation: React.StatelessComponent<SlideAnimationProps> = props => { @@ -18,7 +19,7 @@ export const SlideAnimation: React.StatelessComponent<SlideAnimationProps> = pro } const positionSettings = props.animationState === 'slidIn' ? props.slideInSettings : props.slideOutSettings; return ( - <PositionAnimation positionSettings={positionSettings} zIndex={props.zIndex}> + <PositionAnimation height={props.height} positionSettings={positionSettings} zIndex={props.zIndex}> {props.children} </PositionAnimation> ); diff --git a/packages/instant/src/components/buy_button.tsx b/packages/instant/src/components/buy_button.tsx index 5b07e7416..877ab275c 100644 --- a/packages/instant/src/components/buy_button.tsx +++ b/packages/instant/src/components/buy_button.tsx @@ -1,4 +1,5 @@ import { AssetBuyer, AssetBuyerError, BuyQuote } from '@0x/asset-buyer'; +import { BigNumber } from '@0x/utils'; import { Web3Wrapper } from '@0x/web3-wrapper'; import * as _ from 'lodash'; import * as React from 'react'; @@ -7,16 +8,17 @@ import { oc } from 'ts-optchain'; import { WEB_3_WRAPPER_TRANSACTION_FAILED_ERROR_MSG_PREFIX } from '../constants'; import { ColorOption } from '../style/theme'; import { AffiliateInfo, ZeroExInstantError } from '../types'; -import { getBestAddress } from '../util/address'; -import { balanceUtil } from '../util/balance'; import { gasPriceEstimator } from '../util/gas_price_estimator'; import { util } from '../util/util'; import { Button } from './ui/button'; export interface BuyButtonProps { + accountAddress?: string; + accountEthBalanceInWei?: BigNumber; buyQuote?: BuyQuote; assetBuyer: AssetBuyer; + web3Wrapper: Web3Wrapper; affiliateInfo?: AffiliateInfo; onValidationPending: (buyQuote: BuyQuote) => void; onValidationFail: (buyQuote: BuyQuote, errorMessage: AssetBuyerError | ZeroExInstantError) => void; @@ -33,7 +35,8 @@ export class BuyButton extends React.Component<BuyButtonProps> { onBuyFailure: util.boundNoop, }; public render(): React.ReactNode { - const shouldDisableButton = _.isUndefined(this.props.buyQuote); + const { buyQuote, accountAddress } = this.props; + const shouldDisableButton = _.isUndefined(buyQuote) || _.isUndefined(accountAddress); return ( <Button width="100%" @@ -48,30 +51,25 @@ export class BuyButton extends React.Component<BuyButtonProps> { } private readonly _handleClick = async () => { // The button is disabled when there is no buy quote anyway. - const { buyQuote, assetBuyer, affiliateInfo } = this.props; - if (_.isUndefined(buyQuote)) { + const { buyQuote, assetBuyer, affiliateInfo, accountAddress, accountEthBalanceInWei, web3Wrapper } = this.props; + if (_.isUndefined(buyQuote) || _.isUndefined(accountAddress)) { return; } - this.props.onValidationPending(buyQuote); - - // TODO(bmillman): move address and balance fetching to the async state - const web3Wrapper = new Web3Wrapper(assetBuyer.provider); - const takerAddress = await getBestAddress(web3Wrapper); - - const hasSufficientEth = await balanceUtil.hasSufficientEth(takerAddress, buyQuote, web3Wrapper); + const ethNeededForBuy = buyQuote.worstCaseQuoteInfo.totalEthAmount; + // if we don't have a balance for the user, let the transaction through, it will be handled by the wallet + const hasSufficientEth = _.isUndefined(accountEthBalanceInWei) || accountEthBalanceInWei.gte(ethNeededForBuy); if (!hasSufficientEth) { this.props.onValidationFail(buyQuote, ZeroExInstantError.InsufficientETH); return; } - let txHash: string | undefined; const gasInfo = await gasPriceEstimator.getGasInfoAsync(); const feeRecipient = oc(affiliateInfo).feeRecipient(); try { txHash = await assetBuyer.executeBuyQuoteAsync(buyQuote, { feeRecipient, - takerAddress, + takerAddress: accountAddress, gasPrice: gasInfo.gasPriceInWei, }); } catch (e) { @@ -86,7 +84,6 @@ export class BuyButton extends React.Component<BuyButtonProps> { } throw e; } - const startTimeUnix = new Date().getTime(); const expectedEndTimeUnix = startTimeUnix + gasInfo.estimatedTimeMs; this.props.onBuyProcessing(buyQuote, txHash, startTimeUnix, expectedEndTimeUnix); @@ -99,7 +96,6 @@ export class BuyButton extends React.Component<BuyButtonProps> { } throw e; } - this.props.onBuySuccess(buyQuote, txHash); }; } diff --git a/packages/instant/src/components/buy_order_state_buttons.tsx b/packages/instant/src/components/buy_order_state_buttons.tsx index bdac25cf2..6041bf4f5 100644 --- a/packages/instant/src/components/buy_order_state_buttons.tsx +++ b/packages/instant/src/components/buy_order_state_buttons.tsx @@ -1,4 +1,6 @@ import { AssetBuyer, AssetBuyerError, BuyQuote } from '@0x/asset-buyer'; +import { BigNumber } from '@0x/utils'; +import { Web3Wrapper } from '@0x/web3-wrapper'; import * as React from 'react'; import { ColorOption } from '../style/theme'; @@ -12,9 +14,12 @@ import { Button } from './ui/button'; import { Flex } from './ui/flex'; export interface BuyOrderStateButtonProps { + accountAddress?: string; + accountEthBalanceInWei?: BigNumber; buyQuote?: BuyQuote; buyOrderProcessingState: OrderProcessState; assetBuyer: AssetBuyer; + web3Wrapper: Web3Wrapper; affiliateInfo?: AffiliateInfo; onViewTransaction: () => void; onValidationPending: (buyQuote: BuyQuote) => void; @@ -49,8 +54,11 @@ export const BuyOrderStateButtons: React.StatelessComponent<BuyOrderStateButtonP return ( <BuyButton + accountAddress={props.accountAddress} + accountEthBalanceInWei={props.accountEthBalanceInWei} buyQuote={props.buyQuote} assetBuyer={props.assetBuyer} + web3Wrapper={props.web3Wrapper} affiliateInfo={props.affiliateInfo} onValidationPending={props.onValidationPending} onValidationFail={props.onValidationFail} diff --git a/packages/instant/src/components/css_reset.tsx b/packages/instant/src/components/css_reset.tsx index a1dd2e05c..0bef85389 100644 --- a/packages/instant/src/components/css_reset.tsx +++ b/packages/instant/src/components/css_reset.tsx @@ -27,7 +27,6 @@ export const CSSReset = createGlobalStyle` text-align: left; text-decoration: none; vertical-align: baseline; - z-index: 1; } } `; diff --git a/packages/instant/src/components/erc20_asset_amount_input.tsx b/packages/instant/src/components/erc20_asset_amount_input.tsx index f21c21b87..520ac33d5 100644 --- a/packages/instant/src/components/erc20_asset_amount_input.tsx +++ b/packages/instant/src/components/erc20_asset_amount_input.tsx @@ -113,7 +113,7 @@ export class ERC20AssetAmountInput extends React.Component<ERC20AssetAmountInput } return ( <Container marginLeft="5px"> - <Icon icon="chevron" width={12} onClick={this._handleSelectAssetClick} /> + <Icon icon="chevron" width={12} stroke={ColorOption.white} onClick={this._handleSelectAssetClick} /> </Container> ); }; diff --git a/packages/instant/src/components/erc20_token_selector.tsx b/packages/instant/src/components/erc20_token_selector.tsx index 76d5c66ff..3503ff31a 100644 --- a/packages/instant/src/components/erc20_token_selector.tsx +++ b/packages/instant/src/components/erc20_token_selector.tsx @@ -28,14 +28,14 @@ export class ERC20TokenSelector extends React.Component<ERC20TokenSelectorProps> public render(): React.ReactNode { const { tokens, onTokenSelect } = this.props; return ( - <Container> + <Container height="100%"> <SearchInput placeholder="Search tokens..." width="100%" value={this.state.searchQuery} onChange={this._handleSearchInputChange} /> - <Container overflow="scroll" height={{ default: '275px', sm: '75vh' }} marginTop="10px"> + <Container overflow="scroll" height="calc(100% - 80px)" marginTop="10px"> {_.map(tokens, token => { if (!this._isTokenQueryMatch(token)) { return null; @@ -85,7 +85,7 @@ class TokenSelectorRow extends React.Component<TokenSelectorRowProps> { <Container marginLeft="5px"> <Flex justify="flex-start"> <Container marginRight="10px"> - <Circle diameter={30} fillColor={token.metaData.primaryColor}> + <Circle diameter={30} rawColor={token.metaData.primaryColor}> <Flex height="100%"> <Text fontColor={ColorOption.white} fontSize="8px"> {displaySymbol} diff --git a/packages/instant/src/components/payment_method.tsx b/packages/instant/src/components/payment_method.tsx new file mode 100644 index 000000000..8c0b47d72 --- /dev/null +++ b/packages/instant/src/components/payment_method.tsx @@ -0,0 +1,45 @@ +import { BigNumber } from '@0x/utils'; +import * as _ from 'lodash'; +import * as React from 'react'; + +import { ColorOption } from '../style/theme'; +import { Network } from '../types'; + +import { PaymentMethodDropdown } from './payment_method_dropdown'; +import { Circle } from './ui/circle'; +import { Container } from './ui/container'; +import { Flex } from './ui/flex'; +import { Text } from './ui/text'; + +export interface PaymentMethodProps {} + +export const PaymentMethod: React.StatelessComponent<PaymentMethodProps> = () => ( + <Container padding="20px" width="100%"> + <Container marginBottom="10px"> + <Flex justify="space-between"> + <Text + letterSpacing="1px" + fontColor={ColorOption.primaryColor} + fontWeight={600} + textTransform="uppercase" + fontSize="14px" + > + Payment Method + </Text> + <Flex> + <Circle color={ColorOption.green} diameter={8} /> + <Container marginLeft="3px"> + <Text fontColor={ColorOption.darkGrey} fontSize="12px"> + MetaMask + </Text> + </Container> + </Flex> + </Flex> + </Container> + <PaymentMethodDropdown + accountAddress="0xa1b2c3d4e5f6g7h8j9k10" + accountEthBalanceInWei={new BigNumber(10500000000000000000)} + network={Network.Mainnet} + /> + </Container> +); diff --git a/packages/instant/src/components/payment_method_dropdown.tsx b/packages/instant/src/components/payment_method_dropdown.tsx new file mode 100644 index 000000000..bdce2a49d --- /dev/null +++ b/packages/instant/src/components/payment_method_dropdown.tsx @@ -0,0 +1,44 @@ +import { BigNumber } from '@0x/utils'; +import copy from 'copy-to-clipboard'; +import * as React from 'react'; + +import { Network } from '../types'; +import { etherscanUtil } from '../util/etherscan'; +import { format } from '../util/format'; + +import { Dropdown, DropdownItemConfig } from './ui/dropdown'; + +export interface PaymentMethodDropdownProps { + accountAddress: string; + accountEthBalanceInWei?: BigNumber; + network: Network; +} + +export class PaymentMethodDropdown extends React.Component<PaymentMethodDropdownProps> { + public render(): React.ReactNode { + const { accountAddress, accountEthBalanceInWei } = this.props; + const value = format.ethAddress(accountAddress); + const label = format.ethBaseAmount(accountEthBalanceInWei, 4, '') as string; + return <Dropdown value={value} label={label} items={this._getDropdownItemConfigs()} />; + } + private readonly _getDropdownItemConfigs = (): DropdownItemConfig[] => { + const viewOnEtherscan = { + text: 'View on Etherscan', + onClick: this._handleEtherscanClick, + }; + const copyAddressToClipboard = { + text: 'Copy address to clipboard', + onClick: this._handleCopyToClipboardClick, + }; + return [viewOnEtherscan, copyAddressToClipboard]; + }; + private readonly _handleEtherscanClick = (): void => { + const { accountAddress, network } = this.props; + const etherscanUrl = etherscanUtil.getEtherScanEthAddressIfExists(accountAddress, network); + window.open(etherscanUrl, '_blank'); + }; + private readonly _handleCopyToClipboardClick = (): void => { + const { accountAddress } = this.props; + copy(accountAddress); + }; +} diff --git a/packages/instant/src/components/sliding_error.tsx b/packages/instant/src/components/sliding_error.tsx index 462199d78..a8d4e391c 100644 --- a/packages/instant/src/components/sliding_error.tsx +++ b/packages/instant/src/components/sliding_error.tsx @@ -86,7 +86,7 @@ export const SlidingError: React.StatelessComponent<SlidingErrorProps> = props = <SlideAnimation slideInSettings={slideUpSettings} slideOutSettings={slideOutSettings} - zIndex={{ sm: zIndex.errorPopUp, default: zIndex.errorPopBehind }} + 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 a5d15c401..9d16f9560 100644 --- a/packages/instant/src/components/sliding_panel.tsx +++ b/packages/instant/src/components/sliding_panel.tsx @@ -30,7 +30,9 @@ export const Panel: React.StatelessComponent<PanelProps> = ({ title, children, o <Icon width={12} color={ColorOption.lightGrey} icon="closeX" onClick={onClose} /> </Container> </Flex> - <Container marginTop="10px">{children}</Container> + <Container marginTop="10px" height="100%"> + {children} + </Container> </Container> ); @@ -67,6 +69,7 @@ export const SlidingPanel: React.StatelessComponent<SlidingPanelProps> = props = slideInSettings={slideUpSettings} slideOutSettings={slideDownSettings} animationState={animationState} + height="100%" > <Panel {...rest} /> </SlideAnimation> diff --git a/packages/instant/src/components/ui/circle.tsx b/packages/instant/src/components/ui/circle.tsx index 26764ec71..4f9f56f12 100644 --- a/packages/instant/src/components/ui/circle.tsx +++ b/packages/instant/src/components/ui/circle.tsx @@ -1,24 +1,27 @@ -import { styled } from '../../style/theme'; +import { ColorOption, styled, Theme, withTheme } from '../../style/theme'; export interface CircleProps { diameter: number; - fillColor?: string; + rawColor?: string; + color?: ColorOption; + theme: Theme; } -export const Circle = +export const Circle = withTheme( styled.div < - CircleProps > - ` + CircleProps > + ` && { width: ${props => props.diameter}px; height: ${props => props.diameter}px; - background-color: ${props => props.fillColor}; + background-color: ${props => (props.rawColor ? props.rawColor : props.theme[props.color || ColorOption.white])}; border-radius: 50%; } -`; +`, +); Circle.displayName = 'Circle'; Circle.defaultProps = { - fillColor: 'white', + color: ColorOption.white, }; diff --git a/packages/instant/src/components/ui/container.tsx b/packages/instant/src/components/ui/container.tsx index c42082ed5..8aa5db9e5 100644 --- a/packages/instant/src/components/ui/container.tsx +++ b/packages/instant/src/components/ui/container.tsx @@ -34,6 +34,7 @@ export interface ContainerProps { cursor?: string; overflow?: string; darkenOnHover?: boolean; + boxShadowOnHover?: boolean; flexGrow?: string | number; } @@ -42,7 +43,6 @@ export const Container = ContainerProps > ` && { - all: initial; box-sizing: border-box; ${props => cssRuleIfExists(props, 'flex-grow')} ${props => cssRuleIfExists(props, 'position')} @@ -79,6 +79,7 @@ export const Container = props.backgroundColor ? darken(0.05, props.theme[props.backgroundColor]) : 'none' }` : ''}; + ${props => (props.boxShadowOnHover ? 'box-shadow: 0px 2px 10px rgba(0, 0, 0, 0.1)' : '')}; } } `; diff --git a/packages/instant/src/components/ui/dropdown.tsx b/packages/instant/src/components/ui/dropdown.tsx new file mode 100644 index 000000000..3a23f456d --- /dev/null +++ b/packages/instant/src/components/ui/dropdown.tsx @@ -0,0 +1,134 @@ +import * as _ from 'lodash'; +import * as React from 'react'; + +import { ColorOption, completelyTransparent } from '../../style/theme'; +import { zIndex } from '../../style/z_index'; + +import { Container } from './container'; +import { Flex } from './flex'; +import { Icon } from './icon'; +import { Overlay } from './overlay'; +import { Text } from './text'; + +export interface DropdownItemConfig { + text: string; + onClick?: () => void; +} + +export interface DropdownProps { + value: string; + label?: string; + items: DropdownItemConfig[]; +} + +export interface DropdownState { + isOpen: boolean; +} + +export class Dropdown extends React.Component<DropdownProps, DropdownState> { + public static defaultProps = { + items: [], + }; + public state: DropdownState = { + isOpen: false, + }; + public render(): React.ReactNode { + const { value, label, items } = this.props; + const { isOpen } = this.state; + const hasItems = !_.isEmpty(items); + const borderRadius = isOpen ? '4px 4px 0px 0px' : '4px'; + return ( + <React.Fragment> + {isOpen && ( + <Overlay + zIndex={zIndex.dropdownItems - 1} + backgroundColor={completelyTransparent} + onClick={this._closeDropdown} + /> + )} + <Container position="relative"> + <Container + cursor={hasItems ? 'pointer' : undefined} + onClick={this._handleDropdownClick} + hasBoxShadow={isOpen} + boxShadowOnHover={true} + borderRadius={borderRadius} + border="1px solid" + borderColor={ColorOption.feintGrey} + padding="0.8em" + > + <Flex justify="space-between"> + <Text fontSize="16px" fontColor={ColorOption.darkGrey}> + {value} + </Text> + <Container> + {label && ( + <Text fontSize="16px" fontColor={ColorOption.lightGrey}> + {label} + </Text> + )} + {hasItems && ( + <Container marginLeft="5px" display="inline-block" position="relative" bottom="2px"> + <Icon padding="3px" icon="chevron" width={12} stroke={ColorOption.grey} /> + </Container> + )} + </Container> + </Flex> + </Container> + {isOpen && ( + <Container + width="100%" + position="absolute" + onClick={this._closeDropdown} + backgroundColor={ColorOption.white} + hasBoxShadow={true} + zIndex={zIndex.dropdownItems} + > + {_.map(items, (item, index) => ( + <DropdownItem key={item.text} {...item} isLast={index === items.length - 1} /> + ))} + </Container> + )} + </Container> + </React.Fragment> + ); + } + private readonly _handleDropdownClick = (): void => { + if (_.isEmpty(this.props.items)) { + return; + } + this.setState({ + isOpen: !this.state.isOpen, + }); + }; + private readonly _closeDropdown = (): void => { + this.setState({ + isOpen: false, + }); + }; +} + +export interface DropdownItemProps extends DropdownItemConfig { + text: string; + onClick?: () => void; + isLast: boolean; +} + +export const DropdownItem: React.StatelessComponent<DropdownItemProps> = ({ text, onClick, isLast }) => ( + <Container + onClick={onClick} + cursor="pointer" + darkenOnHover={true} + backgroundColor={ColorOption.white} + padding="0.8em" + borderTop="0" + border="1px solid" + borderRadius={isLast ? '0px 0px 4px 4px' : undefined} + width="100%" + borderColor={ColorOption.feintGrey} + > + <Text fontSize="14px" fontColor={ColorOption.darkGrey}> + {text} + </Text> + </Container> +); diff --git a/packages/instant/src/components/ui/flex.tsx b/packages/instant/src/components/ui/flex.tsx index 5b00138b8..274c46b9e 100644 --- a/packages/instant/src/components/ui/flex.tsx +++ b/packages/instant/src/components/ui/flex.tsx @@ -19,7 +19,6 @@ export const Flex = FlexProps > ` && { - all: initial; display: ${props => (props.inline ? 'inline-flex' : 'flex')}; flex-direction: ${props => props.direction}; flex-wrap: ${props => props.flexWrap}; diff --git a/packages/instant/src/components/ui/icon.tsx b/packages/instant/src/components/ui/icon.tsx index 2679dad1a..a88fa87dd 100644 --- a/packages/instant/src/components/ui/icon.tsx +++ b/packages/instant/src/components/ui/icon.tsx @@ -9,7 +9,6 @@ interface IconInfo { path: string; fillRule?: svgRule; clipRule?: svgRule; - stroke?: string; strokeOpacity?: number; strokeWidth?: number; strokeLinecap?: 'butt' | 'round' | 'square' | 'inherit'; @@ -47,7 +46,6 @@ const ICONS: IconInfoMapping = { chevron: { viewBox: '0 0 12 7', path: 'M11 1L6 6L1 1', - stroke: 'white', strokeOpacity: 0.5, strokeWidth: 1.5, strokeLinecap: 'round', @@ -67,6 +65,7 @@ export interface IconProps { width: number; height?: number; color?: ColorOption; + stroke?: ColorOption; icon: keyof IconInfoMapping; onClick?: (event: React.MouseEvent<HTMLElement>) => void; padding?: string; @@ -75,6 +74,7 @@ export interface IconProps { const PlainIcon: React.StatelessComponent<IconProps> = props => { const iconInfo = ICONS[props.icon]; const colorValue = _.isUndefined(props.color) ? undefined : props.theme[props.color]; + const strokeValue = _.isUndefined(props.stroke) ? undefined : props.theme[props.stroke]; return ( <div onClick={props.onClick} className={props.className}> <svg @@ -89,7 +89,7 @@ const PlainIcon: React.StatelessComponent<IconProps> = props => { fill={colorValue} fillRule={iconInfo.fillRule || 'nonzero'} clipRule={iconInfo.clipRule || 'nonzero'} - stroke={iconInfo.stroke} + stroke={strokeValue} strokeOpacity={iconInfo.strokeOpacity} strokeWidth={iconInfo.strokeWidth} strokeLinecap={iconInfo.strokeLinecap} @@ -102,7 +102,8 @@ const PlainIcon: React.StatelessComponent<IconProps> = props => { export const Icon = withTheme(styled(PlainIcon)` && { - cursor: ${props => (!_.isUndefined(props.onClick) ? 'pointer' : 'default')}; + display: inline-block; + ${props => (!_.isUndefined(props.onClick) ? 'cursor: pointer' : '')}; transition: opacity 0.5s ease; padding: ${props => props.padding}; opacity: ${props => (!_.isUndefined(props.onClick) ? 0.7 : 1)}; diff --git a/packages/instant/src/components/ui/overlay.tsx b/packages/instant/src/components/ui/overlay.tsx index 8c9572615..c5f55f9c0 100644 --- a/packages/instant/src/components/ui/overlay.tsx +++ b/packages/instant/src/components/ui/overlay.tsx @@ -1,29 +1,17 @@ import * as _ from 'lodash'; -import * as React from 'react'; -import { ColorOption, overlayBlack, styled } from '../../style/theme'; - -import { Container } from './container'; -import { Flex } from './flex'; -import { Icon } from './icon'; +import { overlayBlack, styled } from '../../style/theme'; +import { zIndex } from '../../style/z_index'; export interface OverlayProps { - className?: string; - onClose?: () => void; zIndex?: number; + backgroundColor?: string; } -const PlainOverlay: React.StatelessComponent<OverlayProps> = ({ children, className, onClose }) => ( - <Flex height="100vh" className={className}> - <Container position="absolute" top="0px" right="0px" display={{ default: 'initial', sm: 'none' }}> - <Icon height={18} width={18} color={ColorOption.white} icon="closeX" onClick={onClose} padding="2em 2em" /> - </Container> - <Container width={{ default: 'auto', sm: '100%' }} height={{ default: 'auto', sm: '100%' }}> - {children} - </Container> - </Flex> -); -export const Overlay = styled(PlainOverlay)` +export const Overlay = + styled.div < + OverlayProps > + ` && { position: fixed; top: 0; @@ -31,12 +19,13 @@ export const Overlay = styled(PlainOverlay)` bottom: 0; left: 0; z-index: ${props => props.zIndex} - background-color: ${overlayBlack}; + background-color: ${props => props.backgroundColor}; } `; Overlay.defaultProps = { - zIndex: 100, + zIndex: zIndex.overlayDefault, + backgroundColor: overlayBlack, }; Overlay.displayName = 'Overlay'; diff --git a/packages/instant/src/components/ui/text.tsx b/packages/instant/src/components/ui/text.tsx index c6a76ff18..4fe429d25 100644 --- a/packages/instant/src/components/ui/text.tsx +++ b/packages/instant/src/components/ui/text.tsx @@ -28,7 +28,6 @@ export const Text = TextProps > ` && { - all: initial; font-family: 'Inter UI', sans-serif; font-style: ${props => props.fontStyle}; font-weight: ${props => props.fontWeight}; diff --git a/packages/instant/src/components/zero_ex_instant_container.tsx b/packages/instant/src/components/zero_ex_instant_container.tsx index d2216b54f..5748e064e 100644 --- a/packages/instant/src/components/zero_ex_instant_container.tsx +++ b/packages/instant/src/components/zero_ex_instant_container.tsx @@ -3,11 +3,9 @@ import * as React from 'react'; import { AvailableERC20TokenSelector } from '../containers/available_erc20_token_selector'; import { LatestBuyQuoteOrderDetails } from '../containers/latest_buy_quote_order_details'; import { LatestError } from '../containers/latest_error'; +import { SelectedAssetBuyOrderProgress } from '../containers/selected_asset_buy_order_progress'; import { SelectedAssetBuyOrderStateButtons } from '../containers/selected_asset_buy_order_state_buttons'; import { SelectedAssetInstantHeading } from '../containers/selected_asset_instant_heading'; - -import { SelectedAssetBuyOrderProgress } from '../containers/selected_asset_buy_order_progress'; - import { ColorOption } from '../style/theme'; import { zIndex } from '../style/z_index'; diff --git a/packages/instant/src/components/zero_ex_instant_overlay.tsx b/packages/instant/src/components/zero_ex_instant_overlay.tsx index 3461600e1..10438ab7a 100644 --- a/packages/instant/src/components/zero_ex_instant_overlay.tsx +++ b/packages/instant/src/components/zero_ex_instant_overlay.tsx @@ -1,5 +1,10 @@ import * as React from 'react'; +import { ColorOption } from '../style/theme'; + +import { Container } from './ui/container'; +import { Flex } from './ui/flex'; +import { Icon } from './ui/icon'; import { Overlay } from './ui/overlay'; import { ZeroExInstantContainer } from './zero_ex_instant_container'; import { ZeroExInstantProvider, ZeroExInstantProviderProps } from './zero_ex_instant_provider'; @@ -13,8 +18,22 @@ export const ZeroExInstantOverlay: React.StatelessComponent<ZeroExInstantOverlay const { onClose, zIndex, ...rest } = props; return ( <ZeroExInstantProvider {...rest}> - <Overlay onClose={onClose} zIndex={zIndex}> - <ZeroExInstantContainer /> + <Overlay zIndex={zIndex}> + <Flex height="100vh"> + <Container position="absolute" top="0px" right="0px" display={{ default: 'initial', sm: 'none' }}> + <Icon + height={18} + width={18} + color={ColorOption.white} + icon="closeX" + onClick={onClose} + padding="2em 2em" + /> + </Container> + <Container width={{ default: 'auto', sm: '100%' }} height={{ default: 'auto', sm: '100%' }}> + <ZeroExInstantContainer /> + </Container> + </Flex> </Overlay> </ZeroExInstantProvider> ); diff --git a/packages/instant/src/components/zero_ex_instant_provider.tsx b/packages/instant/src/components/zero_ex_instant_provider.tsx index 58e78c522..cceb44377 100644 --- a/packages/instant/src/components/zero_ex_instant_provider.tsx +++ b/packages/instant/src/components/zero_ex_instant_provider.tsx @@ -93,6 +93,8 @@ export class ZeroExInstantProvider extends React.Component<ZeroExInstantProvider asyncData.fetchAvailableAssetDatasAndDispatchToStore(this._store); } // tslint:disable-next-line:no-floating-promises + asyncData.fetchAccountInfoAndDispatchToStore(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 diff --git a/packages/instant/src/constants.ts b/packages/instant/src/constants.ts index 34548f26f..b5c4f96e4 100644 --- a/packages/instant/src/constants.ts +++ b/packages/instant/src/constants.ts @@ -1,6 +1,6 @@ import { BigNumber } from '@0x/utils'; -import { Network } from './types'; +import { AccountNotReady, AccountState, Network } from './types'; export const BIG_NUMBER_ZERO = new BigNumber(0); export const ETH_DECIMALS = 18; @@ -22,3 +22,15 @@ export const ETHEREUM_NODE_URL_BY_NETWORK = { [Network.Kovan]: 'https://kovan.infura.io/', }; export const BLOCK_POLLING_INTERVAL_MS = 10000; // 10s +export const NO_ACCOUNT: AccountNotReady = { + state: AccountState.None, +}; +export const LOADING_ACCOUNT: AccountNotReady = { + state: AccountState.Loading, +}; +export const LOCKED_ACCOUNT: AccountNotReady = { + state: AccountState.Locked, +}; +export const ERROR_ACCOUNT: AccountNotReady = { + state: AccountState.Error, +}; diff --git a/packages/instant/src/containers/selected_asset_buy_order_state_buttons.ts b/packages/instant/src/containers/selected_asset_buy_order_state_buttons.ts index c3a5e88b9..610335243 100644 --- a/packages/instant/src/containers/selected_asset_buy_order_state_buttons.ts +++ b/packages/instant/src/containers/selected_asset_buy_order_state_buttons.ts @@ -1,4 +1,6 @@ import { AssetBuyer, AssetBuyerError, BuyQuote } from '@0x/asset-buyer'; +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'; @@ -7,14 +9,17 @@ import { Dispatch } from 'redux'; import { BuyOrderStateButtons } from '../components/buy_order_state_buttons'; import { Action, actions } from '../redux/actions'; import { State } from '../redux/reducer'; -import { AffiliateInfo, OrderProcessState, ZeroExInstantError } from '../types'; +import { AccountState, AffiliateInfo, OrderProcessState, ZeroExInstantError } from '../types'; import { errorFlasher } from '../util/error_flasher'; import { etherscanUtil } from '../util/etherscan'; interface ConnectedState { + accountAddress?: string; + accountEthBalanceInWei?: BigNumber; buyQuote?: BuyQuote; buyOrderProcessingState: OrderProcessState; assetBuyer: AssetBuyer; + web3Wrapper: Web3Wrapper; affiliateInfo?: AffiliateInfo; onViewTransaction: () => void; } @@ -31,9 +36,16 @@ interface ConnectedDispatch { export interface SelectedAssetBuyOrderStateButtons {} const mapStateToProps = (state: State, _ownProps: SelectedAssetBuyOrderStateButtons): ConnectedState => { const assetBuyer = state.providerState.assetBuyer; + const web3Wrapper = state.providerState.web3Wrapper; + const account = state.providerState.account; + const accountAddress = account.state === AccountState.Ready ? account.address : undefined; + const accountEthBalanceInWei = account.state === AccountState.Ready ? account.ethBalanceInWei : undefined; return { + accountAddress, + accountEthBalanceInWei, buyOrderProcessingState: state.buyOrderState.processState, assetBuyer, + web3Wrapper, buyQuote: state.latestBuyQuote, affiliateInfo: state.affiliateInfo, onViewTransaction: () => { diff --git a/packages/instant/src/redux/actions.ts b/packages/instant/src/redux/actions.ts index c41c5054b..fc89e3d0e 100644 --- a/packages/instant/src/redux/actions.ts +++ b/packages/instant/src/redux/actions.ts @@ -2,7 +2,7 @@ import { BuyQuote } from '@0x/asset-buyer'; import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; -import { ActionsUnion, Asset } from '../types'; +import { ActionsUnion, AddressAndEthBalanceInWei, Asset } from '../types'; export interface PlainAction<T extends string> { type: T; @@ -21,6 +21,11 @@ function createAction<T extends string, P>(type: T, data?: P): PlainAction<T> | } export enum ActionTypes { + SET_ACCOUNT_STATE_LOADING = 'SET_ACCOUNT_STATE_LOADING', + SET_ACCOUNT_STATE_LOCKED = 'SET_ACCOUNT_STATE_LOCKED', + SET_ACCOUNT_STATE_ERROR = 'SET_ACCOUNT_STATE_ERROR', + SET_ACCOUNT_STATE_READY = 'SET_ACCOUNT_STATE_READY', + UPDATE_ACCOUNT_ETH_BALANCE = 'UPDATE_ACCOUNT_ETH_BALANCE', UPDATE_ETH_USD_PRICE = 'UPDATE_ETH_USD_PRICE', UPDATE_SELECTED_ASSET_AMOUNT = 'UPDATE_SELECTED_ASSET_AMOUNT', SET_BUY_ORDER_STATE_NONE = 'SET_BUY_ORDER_STATE_NONE', @@ -40,6 +45,12 @@ export enum ActionTypes { } export const actions = { + setAccountStateLoading: () => createAction(ActionTypes.SET_ACCOUNT_STATE_LOADING), + setAccountStateLocked: () => createAction(ActionTypes.SET_ACCOUNT_STATE_LOCKED), + setAccountStateError: () => createAction(ActionTypes.SET_ACCOUNT_STATE_ERROR), + setAccountStateReady: (address: string) => createAction(ActionTypes.SET_ACCOUNT_STATE_READY, address), + updateAccountEthBalance: (addressAndBalance: AddressAndEthBalanceInWei) => + createAction(ActionTypes.UPDATE_ACCOUNT_ETH_BALANCE, addressAndBalance), updateEthUsdPrice: (price?: BigNumber) => createAction(ActionTypes.UPDATE_ETH_USD_PRICE, price), updateSelectedAssetAmount: (amount?: BigNumber) => createAction(ActionTypes.UPDATE_SELECTED_ASSET_AMOUNT, amount), setBuyOrderStateNone: () => createAction(ActionTypes.SET_BUY_ORDER_STATE_NONE), diff --git a/packages/instant/src/redux/async_data.ts b/packages/instant/src/redux/async_data.ts index 839a90778..a8f632009 100644 --- a/packages/instant/src/redux/async_data.ts +++ b/packages/instant/src/redux/async_data.ts @@ -2,7 +2,7 @@ import { AssetProxyId } from '@0x/types'; import * as _ from 'lodash'; import { BIG_NUMBER_ZERO } from '../constants'; -import { ERC20Asset } from '../types'; +import { AccountState, ERC20Asset } from '../types'; import { assetUtils } from '../util/asset'; import { buyQuoteUpdater } from '../util/buy_quote_updater'; import { coinbaseApi } from '../util/coinbase_api'; @@ -36,6 +36,44 @@ export const asyncData = { store.dispatch(actions.setAvailableAssets([])); } }, + fetchAccountInfoAndDispatchToStore: async (store: Store) => { + const { providerState } = store.getState(); + const web3Wrapper = providerState.web3Wrapper; + if (providerState.account.state !== AccountState.Loading) { + store.dispatch(actions.setAccountStateLoading()); + } + let availableAddresses: string[]; + try { + availableAddresses = await web3Wrapper.getAvailableAddressesAsync(); + } catch (e) { + store.dispatch(actions.setAccountStateError()); + return; + } + if (!_.isEmpty(availableAddresses)) { + const activeAddress = availableAddresses[0]; + store.dispatch(actions.setAccountStateReady(activeAddress)); + // tslint:disable-next-line:no-floating-promises + asyncData.fetchAccountBalanceAndDispatchToStore(store); + } else { + store.dispatch(actions.setAccountStateLocked()); + } + }, + fetchAccountBalanceAndDispatchToStore: async (store: Store) => { + const { providerState } = store.getState(); + const web3Wrapper = providerState.web3Wrapper; + const account = providerState.account; + if (account.state !== AccountState.Ready) { + return; + } + try { + const address = account.address; + const ethBalanceInWei = await web3Wrapper.getBalanceInWeiAsync(address); + store.dispatch(actions.updateAccountEthBalance({ address, ethBalanceInWei })); + } catch (e) { + // leave balance as is + return; + } + }, fetchCurrentBuyQuoteAndDispatchToStore: async (store: Store) => { const { providerState, selectedAsset, selectedAssetAmount, affiliateInfo } = store.getState(); const assetBuyer = providerState.assetBuyer; diff --git a/packages/instant/src/redux/reducer.ts b/packages/instant/src/redux/reducer.ts index 4a939839a..a5a1b6f7d 100644 --- a/packages/instant/src/redux/reducer.ts +++ b/packages/instant/src/redux/reducer.ts @@ -4,8 +4,12 @@ import { BigNumber } from '@0x/utils'; import { Web3Wrapper } from '@0x/web3-wrapper'; import * as _ from 'lodash'; +import { ERROR_ACCOUNT, LOADING_ACCOUNT, LOCKED_ACCOUNT } from '../constants'; import { assetMetaDataMap } from '../data/asset_meta_data_map'; import { + Account, + AccountReady, + AccountState, AffiliateInfo, Asset, AssetMetaData, @@ -57,6 +61,32 @@ export const DEFAULT_STATE: DefaultState = { export const createReducer = (initialState: State) => { const reducer = (state: State = initialState, action: Action): State => { switch (action.type) { + case ActionTypes.SET_ACCOUNT_STATE_LOADING: + return reduceStateWithAccount(state, LOADING_ACCOUNT); + case ActionTypes.SET_ACCOUNT_STATE_LOCKED: + return reduceStateWithAccount(state, LOCKED_ACCOUNT); + case ActionTypes.SET_ACCOUNT_STATE_ERROR: + return reduceStateWithAccount(state, ERROR_ACCOUNT); + case ActionTypes.SET_ACCOUNT_STATE_READY: { + const account: AccountReady = { + state: AccountState.Ready, + address: action.data, + }; + return reduceStateWithAccount(state, account); + } + case ActionTypes.UPDATE_ACCOUNT_ETH_BALANCE: { + const { address, ethBalanceInWei } = action.data; + const currentAccount = state.providerState.account; + if (currentAccount.state !== AccountState.Ready || currentAccount.address !== address) { + return state; + } else { + const newAccount: AccountReady = { + ...currentAccount, + ethBalanceInWei, + }; + return reduceStateWithAccount(state, newAccount); + } + } case ActionTypes.UPDATE_ETH_USD_PRICE: return { ...state, @@ -80,7 +110,6 @@ export const createReducer = (initialState: State) => { } else { return state; } - case ActionTypes.SET_QUOTE_REQUEST_STATE_PENDING: return { ...state, @@ -191,6 +220,18 @@ export const createReducer = (initialState: State) => { return reducer; }; +const reduceStateWithAccount = (state: State, account: Account) => { + const oldProviderState = state.providerState; + const newProviderState: ProviderState = { + ...oldProviderState, + account, + }; + return { + ...state, + providerState: newProviderState, + }; +}; + const doesBuyQuoteMatchState = (buyQuote: BuyQuote, state: State): boolean => { const selectedAssetIfExists = state.selectedAsset; const selectedAssetAmountIfExists = state.selectedAssetAmount; diff --git a/packages/instant/src/style/theme.ts b/packages/instant/src/style/theme.ts index 8dada2d28..2653c38f7 100644 --- a/packages/instant/src/style/theme.ts +++ b/packages/instant/src/style/theme.ts @@ -15,6 +15,8 @@ export enum ColorOption { white = 'white', lightOrange = 'lightOrange', darkOrange = 'darkOrange', + green = 'green', + red = 'red', } export const theme: Theme = { @@ -28,9 +30,12 @@ export const theme: Theme = { white: 'white', lightOrange: '#F9F2ED', darkOrange: '#F2994C', + green: '#3CB34F', + red: '#D00000', }; export const transparentWhite = 'rgba(255,255,255,0.3)'; export const overlayBlack = 'rgba(0, 0, 0, 0.6)'; +export const completelyTransparent = 'rga(0, 0, 0, 0)'; export { styled, css, keyframes, withTheme, createGlobalStyle, ThemeProvider }; diff --git a/packages/instant/src/style/z_index.ts b/packages/instant/src/style/z_index.ts index 03623f044..bd034182e 100644 --- a/packages/instant/src/style/z_index.ts +++ b/packages/instant/src/style/z_index.ts @@ -1,6 +1,8 @@ export const zIndex = { - errorPopBehind: 1, - mainContainer: 2, - panel: 3, - errorPopUp: 4, + errorPopBehind: 10, + mainContainer: 20, + dropdownItems: 30, + panel: 40, + errorPopup: 50, + overlayDefault: 100, }; diff --git a/packages/instant/src/types.ts b/packages/instant/src/types.ts index d65f70008..20ad2ed95 100644 --- a/packages/instant/src/types.ts +++ b/packages/instant/src/types.ts @@ -120,3 +120,8 @@ export interface AccountNotReady { export type Account = AccountReady | AccountNotReady; export type OrderSource = string | SignedOrder[]; + +export interface AddressAndEthBalanceInWei { + address: string; + ethBalanceInWei: BigNumber; +} diff --git a/packages/instant/src/util/balance.ts b/packages/instant/src/util/balance.ts deleted file mode 100644 index f2271495b..000000000 --- a/packages/instant/src/util/balance.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { BuyQuote } from '@0x/asset-buyer'; -import { Web3Wrapper } from '@0x/web3-wrapper'; -import * as _ from 'lodash'; - -export const balanceUtil = { - hasSufficientEth: async (takerAddress: string | undefined, buyQuote: BuyQuote, web3Wrapper: Web3Wrapper) => { - if (_.isUndefined(takerAddress)) { - return false; - } - const balanceWei = await web3Wrapper.getBalanceInWeiAsync(takerAddress); - return balanceWei.gte(buyQuote.worstCaseQuoteInfo.totalEthAmount); - }, -}; diff --git a/packages/instant/src/util/etherscan.ts b/packages/instant/src/util/etherscan.ts index cfc2578a3..4d62c4d9f 100644 --- a/packages/instant/src/util/etherscan.ts +++ b/packages/instant/src/util/etherscan.ts @@ -21,4 +21,11 @@ export const etherscanUtil = { } return `https://${prefix}etherscan.io/tx/${txHash}`; }, + getEtherScanEthAddressIfExists: (ethAddress: string, networkId: number) => { + const prefix = etherscanPrefix(networkId); + if (_.isUndefined(prefix)) { + return; + } + return `https://${prefix}etherscan.io/address/${ethAddress}`; + }, }; diff --git a/packages/instant/src/util/format.ts b/packages/instant/src/util/format.ts index 4a48dec9d..44661d697 100644 --- a/packages/instant/src/util/format.ts +++ b/packages/instant/src/util/format.ts @@ -50,4 +50,7 @@ export const format = { } return `$${ethUnitAmount.mul(ethUsdPrice).toFixed(decimalPlaces)}`; }, + ethAddress: (address: string): string => { + return `0x${address.slice(2, 7)}…${address.slice(-5)}`; + }, }; diff --git a/packages/instant/src/util/provider_state_factory.ts b/packages/instant/src/util/provider_state_factory.ts index 18b188d89..3281f6bfb 100644 --- a/packages/instant/src/util/provider_state_factory.ts +++ b/packages/instant/src/util/provider_state_factory.ts @@ -2,18 +2,12 @@ import { Web3Wrapper } from '@0x/web3-wrapper'; import { Provider } from 'ethereum-types'; import * as _ from 'lodash'; -import { AccountNotReady, AccountState, Maybe, Network, OrderSource, ProviderState } from '../types'; +import { LOADING_ACCOUNT, NO_ACCOUNT } from '../constants'; +import { Maybe, Network, OrderSource, ProviderState } from '../types'; import { assetBuyerFactory } from './asset_buyer_factory'; import { providerFactory } from './provider_factory'; -const LOADING_ACCOUNT: AccountNotReady = { - state: AccountState.Loading, -}; -const NO_ACCOUNT: AccountNotReady = { - state: AccountState.None, -}; - export const providerStateFactory = { getInitialProviderState: (orderSource: OrderSource, network: Network, provider?: Provider): ProviderState => { if (!_.isUndefined(provider)) { |