diff options
Diffstat (limited to 'packages')
27 files changed, 583 insertions, 86 deletions
diff --git a/packages/instant/src/components/animations/slide_animation.tsx b/packages/instant/src/components/animations/slide_animation.tsx index 9adb1c674..5992bcba7 100644 --- a/packages/instant/src/components/animations/slide_animation.tsx +++ b/packages/instant/src/components/animations/slide_animation.tsx @@ -1,10 +1,10 @@ import * as React from 'react'; import { OptionallyScreenSpecific } from '../../style/media'; +import { SlideAnimationState } from '../../types'; import { PositionAnimation, PositionAnimationSettings } from './position_animation'; -export type SlideAnimationState = 'slidIn' | 'slidOut' | 'none'; export interface SlideAnimationProps { animationState: SlideAnimationState; slideInSettings: OptionallyScreenSpecific<PositionAnimationSettings>; diff --git a/packages/instant/src/components/buy_button.tsx b/packages/instant/src/components/buy_button.tsx index 877ab275c..c10e07b83 100644 --- a/packages/instant/src/components/buy_button.tsx +++ b/packages/instant/src/components/buy_button.tsx @@ -43,7 +43,7 @@ export class BuyButton extends React.Component<BuyButtonProps> { onClick={this._handleClick} isDisabled={shouldDisableButton} fontColor={ColorOption.white} - fontSize="20px" + fontSize="16px" > Buy </Button> diff --git a/packages/instant/src/components/buy_order_progress.tsx b/packages/instant/src/components/buy_order_progress.tsx index bc7319423..9c73d58af 100644 --- a/packages/instant/src/components/buy_order_progress.tsx +++ b/packages/instant/src/components/buy_order_progress.tsx @@ -1,6 +1,7 @@ +import * as _ from 'lodash'; import * as React from 'react'; -import { TimedProgressBar } from '../components/timed_progress_bar'; +import { ProgressBar, TimedProgressBar } from '../components/timed_progress_bar'; import { TimeCounter } from '../components/time_counter'; import { Container } from '../components/ui/container'; @@ -12,7 +13,7 @@ export interface BuyOrderProgressProps { export const BuyOrderProgress: React.StatelessComponent<BuyOrderProgressProps> = props => { const { buyOrderState } = props; - + let content: React.ReactNode = null; if ( buyOrderState.processState === OrderProcessState.Processing || buyOrderState.processState === OrderProcessState.Success || @@ -21,15 +22,25 @@ export const BuyOrderProgress: React.StatelessComponent<BuyOrderProgressProps> = const progress = buyOrderState.progress; const hasEnded = buyOrderState.processState !== OrderProcessState.Processing; const expectedTimeMs = progress.expectedEndTimeUnix - progress.startTimeUnix; - return ( - <Container padding="20px 20px 0px 20px" width="100%"> + content = ( + <React.Fragment> <Container marginBottom="5px"> <TimeCounter estimatedTimeMs={expectedTimeMs} hasEnded={hasEnded} key={progress.startTimeUnix} /> </Container> <TimedProgressBar expectedTimeMs={expectedTimeMs} hasEnded={hasEnded} key={progress.startTimeUnix} /> + </React.Fragment> + ); + } else { + // Just show a static progress bar if we aren't processing or in an end state + content = ( + <Container marginTop="10px"> + <ProgressBar width="0px" /> </Container> ); } - - return null; + return ( + <Container padding="20px 20px 0px 20px" width="100%"> + {content} + </Container> + ); }; diff --git a/packages/instant/src/components/erc20_token_selector.tsx b/packages/instant/src/components/erc20_token_selector.tsx index 3503ff31a..d4a77c278 100644 --- a/packages/instant/src/components/erc20_token_selector.tsx +++ b/packages/instant/src/components/erc20_token_selector.tsx @@ -29,13 +29,18 @@ export class ERC20TokenSelector extends React.Component<ERC20TokenSelectorProps> const { tokens, onTokenSelect } = this.props; return ( <Container height="100%"> + <Container marginBottom="10px"> + <Text fontColor={ColorOption.darkGrey} fontSize="18px" fontWeight="600" lineHeight="22px"> + Select Token + </Text> + </Container> <SearchInput placeholder="Search tokens..." width="100%" value={this.state.searchQuery} onChange={this._handleSearchInputChange} /> - <Container overflow="scroll" height="calc(100% - 80px)" marginTop="10px"> + <Container overflow="scroll" height="calc(100% - 90px)" marginTop="10px"> {_.map(tokens, token => { if (!this._isTokenQueryMatch(token)) { return null; diff --git a/packages/instant/src/components/install_wallet_panel_content.tsx b/packages/instant/src/components/install_wallet_panel_content.tsx new file mode 100644 index 000000000..7000e27f6 --- /dev/null +++ b/packages/instant/src/components/install_wallet_panel_content.tsx @@ -0,0 +1,33 @@ +import * as React from 'react'; + +import { META_MASK_CHROME_STORE_URL, META_MASK_SITE_URL } from '../constants'; +import { ColorOption } from '../style/theme'; + +import { MetaMaskLogo } from './meta_mask_logo'; +import { StandardPanelContent } from './standard_panel_content'; +import { Button } from './ui/button'; + +export interface InstallWalletPanelContentProps {} + +export const InstallWalletPanelContent: React.StatelessComponent<InstallWalletPanelContentProps> = () => ( + <StandardPanelContent + image={<MetaMaskLogo width={85} height={80} />} + title="Install MetaMask" + description="Please install the MetaMask wallet extension from the Chrome Store." + moreInfoSettings={{ + href: META_MASK_SITE_URL, + text: 'What is MetaMask?', + }} + action={ + <Button + href={META_MASK_CHROME_STORE_URL} + width="100%" + fontSize="16px" + fontColor={ColorOption.white} + backgroundColor={ColorOption.darkOrange} + > + Get Chrome Extension + </Button> + } + /> +); diff --git a/packages/instant/src/components/meta_mask_logo.tsx b/packages/instant/src/components/meta_mask_logo.tsx new file mode 100644 index 000000000..d1ad10c23 --- /dev/null +++ b/packages/instant/src/components/meta_mask_logo.tsx @@ -0,0 +1,78 @@ +import * as React from 'react'; + +export interface MetaMaskLogoProps { + width?: number; + height?: number; +} + +export const MetaMaskLogo: React.StatelessComponent<MetaMaskLogoProps> = ({ width, height }) => ( + <svg width={width} height={height} viewBox="0 0 85 80" fill="none" xmlns="http://www.w3.org/2000/svg"> + <path d="M80.578 0L47.7107 24.8648L53.542 10.2702L80.578 0Z" fill="#E2761B" /> + <path d="M4.24075 0L37.1081 25.4053L31.2768 10.2702L4.24075 0Z" fill="#E4761B" /> + <path d="M68.9152 57.8379L59.9032 71.8919L78.9874 77.2973L84.2886 58.3785L68.9152 57.8379Z" fill="#E4761B" /> + <path d="M0.53006 58.3785L5.83124 77.2973L24.9155 71.8919L15.9035 57.8379L0.53006 58.3785Z" fill="#E4761B" /> + <path d="M23.8552 34.5941L18.554 42.7022L37.1082 43.7833L36.5781 23.2428L23.8552 34.5941Z" fill="#E4761B" /> + <path d="M60.9635 34.5941L47.7106 23.2428V43.7833L66.2647 42.7022L60.9635 34.5941Z" fill="#E4761B" /> + <path d="M24.9156 71.8914L36.0481 66.4861L26.5059 58.378L24.9156 71.8914Z" fill="#E4761B" /> + <path d="M48.7709 66.4861L59.9034 71.8914L58.313 58.378L48.7709 66.4861Z" fill="#E4761B" /> + <path d="M59.9034 71.8919L48.7709 66.4865L49.301 73.5135V76.7567L59.9034 71.8919Z" fill="#D7C1B3" /> + <path d="M24.9157 71.892L35.518 76.7568V73.5136L36.0482 66.4866L24.9157 71.892Z" fill="#D7C1B3" /> + <path d="M35.5179 53.5138L25.9758 50.8111L32.8673 47.5678L35.5179 53.5138Z" fill="#233447" /> + <path d="M49.3009 53.5138L51.9515 47.5678L58.843 50.8111L49.3009 53.5138Z" fill="#233447" /> + <path d="M24.9155 71.892L26.5059 57.838L15.9035 58.3785L24.9155 71.892Z" fill="#CD6116" /> + <path d="M58.313 57.838L59.9034 71.892L68.9154 58.3785L58.313 57.838Z" fill="#CD6116" /> + <path + d="M66.2648 42.7025L47.7106 43.7836L49.301 53.5132L51.9516 47.5673L58.8431 50.8106L66.2648 42.7025Z" + fill="#CD6116" + /> + <path + d="M25.9758 50.8106L32.8673 47.5673L35.5179 53.5132L37.1083 43.7836L18.5541 42.7025L25.9758 50.8106Z" + fill="#CD6116" + /> + <path d="M18.5541 42.7024L26.5059 58.378L25.9758 50.8105L18.5541 42.7024Z" fill="#E4751F" /> + <path d="M58.8431 50.8106L58.313 58.3781L66.2647 42.7025L58.8431 50.8106Z" fill="#E4751F" /> + <path d="M37.1083 43.7838L35.518 53.5135L37.6384 65.4053L38.1686 49.7297L37.1083 43.7838Z" fill="#E4751F" /> + <path d="M47.7105 43.7838L46.6503 49.7297L47.1804 65.4053L49.3009 53.5135L47.7105 43.7838Z" fill="#E4751F" /> + <path + d="M49.301 53.5134L47.1805 65.4052L48.7709 66.4863L58.313 58.3782L58.8431 50.8107L49.301 53.5134Z" + fill="#F6851B" + /> + <path + d="M25.9758 50.8107L26.5059 58.3782L36.048 66.4863L37.6384 65.4052L35.5179 53.5134L25.9758 50.8107Z" + fill="#F6851B" + /> + <path + d="M49.3011 76.7568V73.5135L48.771 72.973H36.0482L35.518 73.5135V76.7568L24.9157 71.8919L28.6265 75.1351L36.0482 80H48.771L56.1927 75.1351L59.9035 71.8919L49.3011 76.7568Z" + fill="#C0AD9E" + /> + <path + d="M48.771 66.486L47.1806 65.405H37.6385L36.0482 66.486L35.518 73.513L36.0482 72.9725H48.771L49.3011 73.513L48.771 66.486Z" + fill="#161616" + /> + <path + d="M82.1685 26.4864L84.8191 12.9729L80.5781 0L48.771 24.3242L60.9637 34.5945L78.4576 39.9998L82.1685 35.6755L80.5781 34.0539L83.2287 31.8918L81.1082 30.2702L83.7588 28.108L82.1685 26.4864Z" + fill="#763D16" + /> + <path + d="M0 12.9729L2.65059 26.4864L1.06024 28.108L3.71083 30.2702L1.59036 31.8918L4.24095 34.0539L2.65059 35.6755L6.36142 39.9998L23.8553 34.5945L36.0481 24.3242L4.24095 0L0 12.9729Z" + fill="#763D16" + /> + <path + d="M78.4575 39.9993L60.9636 34.5939L66.2648 42.702L58.313 58.3776H68.9154H84.2888L78.4575 39.9993Z" + fill="#F6851B" + /> + <path + d="M23.8554 34.5939L6.36147 39.9993L0.530167 58.3776H15.9036H26.506L18.5542 42.702L23.8554 34.5939Z" + fill="#F6851B" + /> + <path + d="M47.7106 43.7833L48.7709 24.3239L53.5419 10.2699H31.2769L36.048 24.3239L37.1083 43.7833L37.6384 49.7292V65.4048H47.1805V49.7292L47.7106 43.7833Z" + fill="#F6851B" + /> + </svg> +); + +MetaMaskLogo.defaultProps = { + width: 85, + height: 80, +}; diff --git a/packages/instant/src/components/payment_method.tsx b/packages/instant/src/components/payment_method.tsx index 8c0b47d72..3bbe3f1a0 100644 --- a/packages/instant/src/components/payment_method.tsx +++ b/packages/instant/src/components/payment_method.tsx @@ -1,45 +1,138 @@ -import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; import * as React from 'react'; import { ColorOption } from '../style/theme'; -import { Network } from '../types'; +import { Account, AccountState, Network, StandardSlidingPanelContent } from '../types'; +import { MetaMaskLogo } from './meta_mask_logo'; import { PaymentMethodDropdown } from './payment_method_dropdown'; import { Circle } from './ui/circle'; import { Container } from './ui/container'; import { Flex } from './ui/flex'; +import { Icon } from './ui/icon'; import { Text } from './ui/text'; -export interface PaymentMethodProps {} +export interface PaymentMethodProps { + account: Account; + network: Network; + openStandardSlidingPanel: (content: StandardSlidingPanelContent) => void; +} -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} /> +export class PaymentMethod extends React.Component<PaymentMethodProps> { + public render(): React.ReactNode { + return ( + <Container padding="20px" width="100%"> + <Container marginBottom="12px"> + <Flex justify="space-between"> + <Text + letterSpacing="1px" + fontColor={ColorOption.primaryColor} + fontWeight={600} + textTransform="uppercase" + fontSize="14px" + > + {this._renderTitleText()} + </Text> + <Flex>{this._renderTitleLabel()}</Flex> + </Flex> + </Container> + {this._renderMainContent()} + </Container> + ); + } + private readonly _renderTitleText = (): string => { + const { account } = this.props; + switch (account.state) { + case AccountState.Loading: + return 'loading...'; + case AccountState.Error: + case AccountState.Locked: + case AccountState.None: + return 'connect your wallet'; + case AccountState.Ready: + return 'payment method'; + default: + return 'payment method'; + } + }; + private readonly _renderTitleLabel = (): React.ReactNode => { + const { account } = this.props; + if (account.state === AccountState.Ready || account.state === AccountState.Locked) { + const circleColor: ColorOption = account.state === AccountState.Ready ? ColorOption.green : ColorOption.red; + return ( + <React.Fragment> + <Circle diameter={8} color={circleColor} /> <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} - /> + </React.Fragment> + ); + } + return null; + }; + private readonly _renderMainContent = (): React.ReactNode => { + const { account, network } = this.props; + switch (account.state) { + case AccountState.Loading: + return <Container height="48px" />; + case AccountState.Locked: + return ( + <WalletPrompt + onClick={this._openInstallWalletPanel} + image={<Icon width={13} icon="lock" color={ColorOption.black} />} + > + Please Unlock MetaMask + </WalletPrompt> + ); + case AccountState.None: + return ( + <WalletPrompt + onClick={this._openInstallWalletPanel} + image={<MetaMaskLogo width={19} height={18} />} + > + Install MetaMask + </WalletPrompt> + ); + case AccountState.Ready: + return ( + <PaymentMethodDropdown + accountAddress={account.address} + accountEthBalanceInWei={account.ethBalanceInWei} + network={network} + /> + ); + default: + return null; + } + }; + private readonly _openInstallWalletPanel = () => { + this.props.openStandardSlidingPanel(StandardSlidingPanelContent.InstallWallet); + }; +} + +interface WalletPromptProps { + image: React.ReactNode; + onClick?: () => void; +} + +const WalletPrompt: React.StatelessComponent<WalletPromptProps> = ({ onClick, image, children }) => ( + <Container + padding="12px" + border={`1px solid ${ColorOption.darkOrange}`} + backgroundColor={ColorOption.lightOrange} + width="100%" + borderRadius="4px" + onClick={onClick} + cursor={onClick ? 'pointer' : undefined} + boxShadowOnHover={!!onClick} + > + <Flex> + <Container marginRight="10px">{image}</Container> + <Text fontSize="16px" fontColor={ColorOption.darkOrange}> + {children} + </Text> + </Flex> </Container> ); diff --git a/packages/instant/src/components/sliding_error.tsx b/packages/instant/src/components/sliding_error.tsx index a8d4e391c..b59e2a905 100644 --- a/packages/instant/src/components/sliding_error.tsx +++ b/packages/instant/src/components/sliding_error.tsx @@ -3,9 +3,10 @@ import * as React from 'react'; import { ScreenSpecification } from '../style/media'; import { ColorOption } from '../style/theme'; import { zIndex } from '../style/z_index'; +import { SlideAnimationState } from '../types'; import { PositionAnimationSettings } from './animations/position_animation'; -import { SlideAnimation, SlideAnimationState } from './animations/slide_animation'; +import { SlideAnimation } from './animations/slide_animation'; import { Container } from './ui/container'; import { Flex } from './ui/flex'; diff --git a/packages/instant/src/components/sliding_panel.tsx b/packages/instant/src/components/sliding_panel.tsx index 9d16f9560..7f9037049 100644 --- a/packages/instant/src/components/sliding_panel.tsx +++ b/packages/instant/src/components/sliding_panel.tsx @@ -2,35 +2,25 @@ import * as React from 'react'; import { ColorOption } from '../style/theme'; import { zIndex } from '../style/z_index'; +import { SlideAnimationState } from '../types'; import { PositionAnimationSettings } from './animations/position_animation'; -import { SlideAnimation, SlideAnimationState } from './animations/slide_animation'; +import { SlideAnimation } from './animations/slide_animation'; import { Container } from './ui/container'; import { Flex } from './ui/flex'; import { Icon } from './ui/icon'; -import { Text } from './ui/text'; export interface PanelProps { - title?: string; onClose?: () => void; } -export const Panel: React.StatelessComponent<PanelProps> = ({ title, children, onClose }) => ( +export const Panel: React.StatelessComponent<PanelProps> = ({ children, onClose }) => ( <Container backgroundColor={ColorOption.white} width="100%" height="100%" zIndex={zIndex.panel} padding="20px"> - <Flex justify="space-between"> - {title && ( - <Container marginTop="3px"> - <Text fontColor={ColorOption.darkGrey} fontSize="18px" fontWeight="600" lineHeight="22px"> - {title} - </Text> - </Container> - )} - <Container position="relative" bottom="7px"> - <Icon width={12} color={ColorOption.lightGrey} icon="closeX" onClick={onClose} /> - </Container> + <Flex justify="flex-end"> + <Icon padding="5px" width={12} color={ColorOption.lightGrey} icon="closeX" onClick={onClose} /> </Flex> - <Container marginTop="10px" height="100%"> + <Container position="relative" top="-10px" height="100%"> {children} </Container> </Container> diff --git a/packages/instant/src/components/standard_panel_content.tsx b/packages/instant/src/components/standard_panel_content.tsx new file mode 100644 index 000000000..95a79bd55 --- /dev/null +++ b/packages/instant/src/components/standard_panel_content.tsx @@ -0,0 +1,60 @@ +import * as React from 'react'; + +import { ColorOption } from '../style/theme'; + +import { Container } from './ui/container'; +import { Flex } from './ui/flex'; +import { Text } from './ui/text'; + +export interface MoreInfoSettings { + text: string; + href: string; +} + +export interface StandardPanelContentProps { + image: React.ReactNode; + title: string; + description: string; + moreInfoSettings?: MoreInfoSettings; + action: React.ReactNode; +} + +const spacingBetweenPx = '20px'; + +export const StandardPanelContent: React.StatelessComponent<StandardPanelContentProps> = ({ + image, + title, + description, + moreInfoSettings, + action, +}) => ( + <Container height="100%"> + <Flex direction="column" height="calc(100% - 58px)"> + <Container marginBottom={spacingBetweenPx}>{image}</Container> + <Container marginBottom={spacingBetweenPx}> + <Text fontSize="20px" fontWeight={700} fontColor={ColorOption.black}> + {title} + </Text> + </Container> + <Container marginBottom={spacingBetweenPx}> + <Text fontSize="14px" fontColor={ColorOption.grey} center={true}> + {description} + </Text> + </Container> + <Container marginBottom={spacingBetweenPx}> + {moreInfoSettings && ( + <Text + center={true} + fontSize="13px" + textDecorationLine="underline" + fontColor={ColorOption.lightGrey} + href={moreInfoSettings.href} + > + {moreInfoSettings.text} + </Text> + )} + </Container> + </Flex> + <Container>{action}</Container> + </Container> +); diff --git a/packages/instant/src/components/standard_sliding_panel.tsx b/packages/instant/src/components/standard_sliding_panel.tsx new file mode 100644 index 000000000..9409d9664 --- /dev/null +++ b/packages/instant/src/components/standard_sliding_panel.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; + +import { SlideAnimationState, StandardSlidingPanelContent, StandardSlidingPanelSettings } from '../types'; + +import { InstallWalletPanelContent } from './install_wallet_panel_content'; +import { SlidingPanel } from './sliding_panel'; + +export interface StandardSlidingPanelProps extends StandardSlidingPanelSettings { + onClose: () => void; +} + +export class StandardSlidingPanel extends React.Component<StandardSlidingPanelProps> { + public render(): React.ReactNode { + const { animationState, content, onClose } = this.props; + return ( + <SlidingPanel animationState={animationState} onClose={onClose}> + {this._getNodeForContent(content)} + </SlidingPanel> + ); + } + private readonly _getNodeForContent = (content: StandardSlidingPanelContent): React.ReactNode => { + switch (content) { + case StandardSlidingPanelContent.InstallWallet: + return <InstallWalletPanelContent />; + default: + return null; + } + }; +} diff --git a/packages/instant/src/components/timed_progress_bar.tsx b/packages/instant/src/components/timed_progress_bar.tsx index 59aaa33a1..8465b9cd0 100644 --- a/packages/instant/src/components/timed_progress_bar.tsx +++ b/packages/instant/src/components/timed_progress_bar.tsx @@ -2,7 +2,7 @@ import * as _ from 'lodash'; import * as React from 'react'; import { PROGRESS_FINISH_ANIMATION_TIME_MS, PROGRESS_STALL_AT_WIDTH } from '../constants'; -import { ColorOption, keyframes, styled } from '../style/theme'; +import { ColorOption, css, keyframes, styled } from '../style/theme'; import { Container } from './ui/container'; @@ -20,15 +20,11 @@ export class TimedProgressBar extends React.Component<TimedProgressBarProps, {}> private readonly _barRef = React.createRef<HTMLDivElement>(); public render(): React.ReactNode { - const timedProgressProps = this._calculateTimedProgressProps(); - return ( - <Container width="100%" backgroundColor={ColorOption.lightGrey} borderRadius="6px"> - <TimedProgress {...timedProgressProps} ref={this._barRef as any} /> - </Container> - ); + const widthAnimationSettings = this._calculateWidthAnimationSettings(); + return <ProgressBar animationSettings={widthAnimationSettings} ref={this._barRef} />; } - private _calculateTimedProgressProps(): TimedProgressProps { + private _calculateWidthAnimationSettings(): WidthAnimationSettings { if (this.props.hasEnded) { if (!this._barRef.current) { throw new Error('ended but no reference'); @@ -60,21 +56,45 @@ const expandingWidthKeyframes = (fromWidth: string, toWidth: string) => { `; }; -interface TimedProgressProps { +export interface WidthAnimationSettings { timeMs: number; fromWidth: string; toWidth: string; } -export const TimedProgress = +interface ProgressProps { + width?: string; + animationSettings?: WidthAnimationSettings; +} + +export const Progress = styled.div < - TimedProgressProps > + ProgressProps > ` && { background-color: ${props => props.theme[ColorOption.primaryColor]}; border-radius: 6px; height: 6px; - animation: ${props => expandingWidthKeyframes(props.fromWidth, props.toWidth)} - ${props => props.timeMs}ms linear 1 forwards; + ${props => (props.width ? `width: ${props.width};` : '')} + ${props => + props.animationSettings + ? css` + animation: ${expandingWidthKeyframes( + props.animationSettings.fromWidth, + props.animationSettings.toWidth, + )} + ${props.animationSettings.timeMs}ms linear 1 forwards; + ` + : ''} } `; + +export interface ProgressBarProps extends ProgressProps {} + +export const ProgressBar: React.ComponentType<ProgressBarProps & React.ClassAttributes<{}>> = React.forwardRef( + (props, ref) => ( + <Container width="100%" backgroundColor={ColorOption.lightGrey} borderRadius="6px"> + <Progress {...props} ref={ref as any} /> + </Container> + ), +); diff --git a/packages/instant/src/components/ui/button.tsx b/packages/instant/src/components/ui/button.tsx index b90221bf4..fbc5bcad4 100644 --- a/packages/instant/src/components/ui/button.tsx +++ b/packages/instant/src/components/ui/button.tsx @@ -2,6 +2,9 @@ import { darken, saturate } from 'polished'; import * as React from 'react'; import { ColorOption, styled } from '../../style/theme'; +import { util } from '../../util/util'; + +export type ButtonOnClickHandler = (event: React.MouseEvent<HTMLElement>) => void; export interface ButtonProps { backgroundColor?: ColorOption; @@ -12,15 +15,26 @@ export interface ButtonProps { padding?: string; type?: string; isDisabled?: boolean; - onClick?: (event: React.MouseEvent<HTMLElement>) => void; + href?: string; + onClick?: ButtonOnClickHandler; className?: string; } -const PlainButton: React.StatelessComponent<ButtonProps> = ({ children, isDisabled, onClick, type, className }) => ( - <button type={type} className={className} onClick={isDisabled ? undefined : onClick} disabled={isDisabled}> - {children} - </button> -); +const PlainButton: React.StatelessComponent<ButtonProps> = ({ + children, + isDisabled, + onClick, + href, + type, + className, +}) => { + const computedOnClick = isDisabled ? undefined : href ? util.createHrefOnClick(href) : onClick; + return ( + <button type={type} className={className} onClick={computedOnClick} disabled={isDisabled}> + {children} + </button> + ); +}; const darkenOnHoverAmount = 0.1; const darkenOnActiveAmount = 0.2; @@ -31,7 +45,7 @@ export const Button = styled(PlainButton)` box-sizing: border-box; font-size: ${props => props.fontSize}; font-family: 'Inter UI', sans-serif; - font-weight: 600; + font-weight: 500; color: ${props => props.fontColor && props.theme[props.fontColor]}; cursor: ${props => (props.isDisabled ? 'default' : 'pointer')}; transition: background-color, opacity 0.5s ease; @@ -64,10 +78,9 @@ export const Button = styled(PlainButton)` Button.defaultProps = { backgroundColor: ColorOption.primaryColor, - borderColor: ColorOption.primaryColor, width: 'auto', isDisabled: false, - padding: '.6em 1.2em', + padding: '.82em 1.2em', fontSize: '15px', }; diff --git a/packages/instant/src/components/ui/icon.tsx b/packages/instant/src/components/ui/icon.tsx index a88fa87dd..811142b5b 100644 --- a/packages/instant/src/components/ui/icon.tsx +++ b/packages/instant/src/components/ui/icon.tsx @@ -20,6 +20,7 @@ interface IconInfoMapping { success: IconInfo; chevron: IconInfo; search: IconInfo; + lock: IconInfo; } const ICONS: IconInfoMapping = { closeX: { @@ -58,6 +59,11 @@ const ICONS: IconInfoMapping = { path: 'M8.39404 5.19727C8.39404 6.96289 6.96265 8.39453 5.19702 8.39453C3.4314 8.39453 2 6.96289 2 5.19727C2 3.43164 3.4314 2 5.19702 2C6.96265 2 8.39404 3.43164 8.39404 5.19727ZM8.09668 9.51074C7.26855 10.0684 6.27075 10.3945 5.19702 10.3945C2.3269 10.3945 0 8.06738 0 5.19727C0 2.32715 2.3269 0 5.19702 0C8.06738 0 10.394 2.32715 10.394 5.19727C10.394 6.27051 10.0686 7.26855 9.51074 8.09668L13.6997 12.2861L12.2854 13.7002L8.09668 9.51074Z', }, + lock: { + viewBox: '0 0 13 16', + path: + 'M6.47619 0C3.79509 0 1.60489 2.21216 1.60489 4.92014V6.33135C0.717479 6.33135 0 7.05602 0 7.95232V14.379C0 15.2753 0.717479 16 1.60489 16H11.3475C12.2349 16 12.9524 15.2753 12.9524 14.379V7.95232C12.9524 7.05602 12.2349 6.33135 11.3475 6.33135V4.92014C11.3475 2.21216 9.1573 0 6.47619 0ZM9.6482 6.33135H3.30418V4.92014C3.30418 3.16567 4.72026 1.71633 6.47619 1.71633C8.23213 1.71633 9.6482 3.16567 9.6482 4.92014V6.33135Z', + }, }; export interface IconProps { diff --git a/packages/instant/src/components/ui/text.tsx b/packages/instant/src/components/ui/text.tsx index 4fe429d25..3dba11c74 100644 --- a/packages/instant/src/components/ui/text.tsx +++ b/packages/instant/src/components/ui/text.tsx @@ -2,6 +2,7 @@ import { darken } from 'polished'; import * as React from 'react'; import { ColorOption, styled } from '../../style/theme'; +import { util } from '../../util/util'; export interface TextProps { fontColor?: ColorOption; @@ -20,10 +21,16 @@ export interface TextProps { onClick?: (event: React.MouseEvent<HTMLElement>) => void; noWrap?: boolean; display?: string; + href?: string; } +export const Text: React.StatelessComponent<TextProps> = ({ href, onClick, ...rest }) => { + const computedOnClick = href ? util.createHrefOnClick(href) : onClick; + return <StyledText {...rest} onClick={computedOnClick} />; +}; + const darkenOnHoverAmount = 0.3; -export const Text = +export const StyledText = styled.div < TextProps > ` diff --git a/packages/instant/src/components/zero_ex_instant.tsx b/packages/instant/src/components/zero_ex_instant.tsx index b945f9908..f6ee28dba 100644 --- a/packages/instant/src/components/zero_ex_instant.tsx +++ b/packages/instant/src/components/zero_ex_instant.tsx @@ -1,8 +1,8 @@ import * as React from 'react'; import { INJECTED_DIV_CLASS } from '../constants'; +import { ConnectedZeroExInstantContainer } from '../containers/connected_zero_ex_instant_container'; -import { ZeroExInstantContainer } from './zero_ex_instant_container'; import { ZeroExInstantProvider, ZeroExInstantProviderProps } from './zero_ex_instant_provider'; export type ZeroExInstantProps = ZeroExInstantProviderProps; @@ -11,7 +11,7 @@ export const ZeroExInstant: React.StatelessComponent<ZeroExInstantProps> = props return ( <div className={INJECTED_DIV_CLASS}> <ZeroExInstantProvider {...props}> - <ZeroExInstantContainer /> + <ConnectedZeroExInstantContainer /> </ZeroExInstantProvider> </div> ); diff --git a/packages/instant/src/components/zero_ex_instant_container.tsx b/packages/instant/src/components/zero_ex_instant_container.tsx index 5748e064e..6412c9c66 100644 --- a/packages/instant/src/components/zero_ex_instant_container.tsx +++ b/packages/instant/src/components/zero_ex_instant_container.tsx @@ -1,6 +1,8 @@ import * as React from 'react'; import { AvailableERC20TokenSelector } from '../containers/available_erc20_token_selector'; +import { ConnectedAccountPaymentMethod } from '../containers/connected_account_payment_method'; +import { CurrentStandardSlidingPanel } from '../containers/current_standard_sliding_panel'; import { LatestBuyQuoteOrderDetails } from '../containers/latest_buy_quote_order_details'; import { LatestError } from '../containers/latest_error'; import { SelectedAssetBuyOrderProgress } from '../containers/selected_asset_buy_order_progress'; @@ -8,14 +10,16 @@ import { SelectedAssetBuyOrderStateButtons } from '../containers/selected_asset_ import { SelectedAssetInstantHeading } from '../containers/selected_asset_instant_heading'; import { ColorOption } from '../style/theme'; import { zIndex } from '../style/z_index'; +import { OrderProcessState, SlideAnimationState } from '../types'; -import { SlideAnimationState } from './animations/slide_animation'; import { CSSReset } from './css_reset'; import { SlidingPanel } from './sliding_panel'; import { Container } from './ui/container'; import { Flex } from './ui/flex'; -export interface ZeroExInstantContainerProps {} +export interface ZeroExInstantContainerProps { + orderProcessState: OrderProcessState; +} export interface ZeroExInstantContainerState { tokenSelectionPanelAnimationState: SlideAnimationState; } @@ -47,19 +51,19 @@ export class ZeroExInstantContainer extends React.Component<ZeroExInstantContain > <Flex direction="column" justify="flex-start" height="100%"> <SelectedAssetInstantHeading onSelectAssetClick={this._handleSymbolClick} /> - <SelectedAssetBuyOrderProgress /> + {this._renderPaymentMethodOrBuyOrderProgress()} <LatestBuyQuoteOrderDetails /> <Container padding="20px" width="100%"> <SelectedAssetBuyOrderStateButtons /> </Container> </Flex> <SlidingPanel - title="Select Token" animationState={this.state.tokenSelectionPanelAnimationState} onClose={this._handlePanelClose} > <AvailableERC20TokenSelector onTokenSelect={this._handlePanelClose} /> </SlidingPanel> + <CurrentStandardSlidingPanel /> </Container> </Container> </React.Fragment> @@ -75,4 +79,18 @@ export class ZeroExInstantContainer extends React.Component<ZeroExInstantContain tokenSelectionPanelAnimationState: 'slidOut', }); }; + private readonly _renderPaymentMethodOrBuyOrderProgress = (): React.ReactNode => { + const { orderProcessState } = this.props; + if ( + orderProcessState === OrderProcessState.Processing || + orderProcessState === OrderProcessState.Success || + orderProcessState === OrderProcessState.Failure + ) { + return <SelectedAssetBuyOrderProgress />; + } + if (orderProcessState === OrderProcessState.None) { + return <ConnectedAccountPaymentMethod />; + } + return null; + }; } diff --git a/packages/instant/src/components/zero_ex_instant_overlay.tsx b/packages/instant/src/components/zero_ex_instant_overlay.tsx index 10438ab7a..a7e1bd65a 100644 --- a/packages/instant/src/components/zero_ex_instant_overlay.tsx +++ b/packages/instant/src/components/zero_ex_instant_overlay.tsx @@ -1,12 +1,12 @@ import * as React from 'react'; +import { ConnectedZeroExInstantContainer } from '../containers/connected_zero_ex_instant_container'; 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'; export interface ZeroExInstantOverlayProps extends ZeroExInstantProviderProps { @@ -31,7 +31,7 @@ export const ZeroExInstantOverlay: React.StatelessComponent<ZeroExInstantOverlay /> </Container> <Container width={{ default: 'auto', sm: '100%' }} height={{ default: 'auto', sm: '100%' }}> - <ZeroExInstantContainer /> + <ConnectedZeroExInstantContainer /> </Container> </Flex> </Overlay> diff --git a/packages/instant/src/constants.ts b/packages/instant/src/constants.ts index b5c4f96e4..4db73845b 100644 --- a/packages/instant/src/constants.ts +++ b/packages/instant/src/constants.ts @@ -17,6 +17,10 @@ export const ETH_GAS_STATION_API_BASE_URL = 'https://ethgasstation.info'; export const COINBASE_API_BASE_URL = 'https://api.coinbase.com/v2'; export const PROGRESS_STALL_AT_WIDTH = '95%'; export const PROGRESS_FINISH_ANIMATION_TIME_MS = 200; +export const NULL_ADDRESS = '0x0000000000000000000000000000000000000000'; +export const META_MASK_CHROME_STORE_URL = + 'https://chrome.google.com/webstore/detail/metamask/nkbihfbeogaeaoehlefnkodbefgpgknn?hl=en'; +export const META_MASK_SITE_URL = 'https://metamask.io/'; export const ETHEREUM_NODE_URL_BY_NETWORK = { [Network.Mainnet]: 'https://mainnet.infura.io/', [Network.Kovan]: 'https://kovan.infura.io/', diff --git a/packages/instant/src/containers/connected_account_payment_method.ts b/packages/instant/src/containers/connected_account_payment_method.ts new file mode 100644 index 000000000..ee57d1829 --- /dev/null +++ b/packages/instant/src/containers/connected_account_payment_method.ts @@ -0,0 +1,37 @@ +import * as React from 'react'; +import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; + +import { PaymentMethod } from '../components/payment_method'; +import { Action, actions } from '../redux/actions'; +import { State } from '../redux/reducer'; +import { Account, Network, StandardSlidingPanelContent } from '../types'; + +export interface ConnectedAccountPaymentMethodProps {} + +interface ConnectedState { + account: Account; + network: Network; +} + +interface ConnectedDispatch { + openStandardSlidingPanel: (content: StandardSlidingPanelContent) => void; +} + +const mapStateToProps = (state: State, _ownProps: ConnectedAccountPaymentMethodProps): ConnectedState => ({ + account: state.providerState.account, + network: state.network, +}); + +const mapDispatchToProps = ( + dispatch: Dispatch<Action>, + ownProps: ConnectedAccountPaymentMethodProps, +): ConnectedDispatch => ({ + openStandardSlidingPanel: (content: StandardSlidingPanelContent) => + dispatch(actions.openStandardSlidingPanel(content)), +}); + +export const ConnectedAccountPaymentMethod: React.ComponentClass<ConnectedAccountPaymentMethodProps> = connect( + mapStateToProps, + mapDispatchToProps, +)(PaymentMethod); diff --git a/packages/instant/src/containers/connected_zero_ex_instant_container.ts b/packages/instant/src/containers/connected_zero_ex_instant_container.ts new file mode 100644 index 000000000..9606d18c2 --- /dev/null +++ b/packages/instant/src/containers/connected_zero_ex_instant_container.ts @@ -0,0 +1,21 @@ +import * as React from 'react'; +import { connect } from 'react-redux'; + +import { State } from '../redux/reducer'; +import { OrderProcessState } from '../types'; + +import { ZeroExInstantContainer } from '../components/zero_ex_instant_container'; + +export interface ConnectedZeroExInstantContainerProps {} + +interface ConnectedState { + orderProcessState: OrderProcessState; +} + +const mapStateToProps = (state: State, _ownProps: ConnectedZeroExInstantContainerProps): ConnectedState => ({ + orderProcessState: state.buyOrderState.processState, +}); + +export const ConnectedZeroExInstantContainer: React.ComponentClass<ConnectedZeroExInstantContainerProps> = connect( + mapStateToProps, +)(ZeroExInstantContainer); diff --git a/packages/instant/src/containers/current_standard_sliding_panel.ts b/packages/instant/src/containers/current_standard_sliding_panel.ts new file mode 100644 index 000000000..82ac7fa1b --- /dev/null +++ b/packages/instant/src/containers/current_standard_sliding_panel.ts @@ -0,0 +1,31 @@ +import * as React from 'react'; +import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; + +import { StandardSlidingPanel } from '../components/standard_sliding_panel'; +import { Action, actions } from '../redux/actions'; +import { State } from '../redux/reducer'; +import { StandardSlidingPanelSettings } from '../types'; + +export interface CurrentStandardSlidingPanelProps {} + +interface ConnectedState extends StandardSlidingPanelSettings {} + +interface ConnectedDispatch { + onClose: () => void; +} + +const mapStateToProps = (state: State, _ownProps: CurrentStandardSlidingPanelProps): ConnectedState => + state.standardSlidingPanelSettings; + +const mapDispatchToProps = ( + dispatch: Dispatch<Action>, + ownProps: CurrentStandardSlidingPanelProps, +): ConnectedDispatch => ({ + onClose: () => dispatch(actions.closeStandardSlidingPanel()), +}); + +export const CurrentStandardSlidingPanel: React.ComponentClass<CurrentStandardSlidingPanelProps> = connect( + mapStateToProps, + mapDispatchToProps, +)(StandardSlidingPanel); diff --git a/packages/instant/src/containers/latest_error.tsx b/packages/instant/src/containers/latest_error.tsx index c0da181f1..b7cfdb504 100644 --- a/packages/instant/src/containers/latest_error.tsx +++ b/packages/instant/src/containers/latest_error.tsx @@ -3,7 +3,6 @@ import * as React from 'react'; import { connect } from 'react-redux'; import { Dispatch } from 'redux'; -import { SlideAnimationState } from '../components/animations/slide_animation'; import { SlidingError } from '../components/sliding_error'; import { Overlay } from '../components/ui/overlay'; import { Action } from '../redux/actions'; @@ -11,7 +10,7 @@ import { State } from '../redux/reducer'; import { ScreenWidths } from '../style/media'; import { generateOverlayBlack } from '../style/theme'; import { zIndex } from '../style/z_index'; -import { Asset, DisplayStatus, Omit } from '../types'; +import { Asset, DisplayStatus, Omit, SlideAnimationState } from '../types'; import { errorFlasher } from '../util/error_flasher'; export interface LatestErrorComponentProps { diff --git a/packages/instant/src/redux/actions.ts b/packages/instant/src/redux/actions.ts index fc89e3d0e..a899da23c 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, AddressAndEthBalanceInWei, Asset } from '../types'; +import { ActionsUnion, AddressAndEthBalanceInWei, Asset, StandardSlidingPanelContent } from '../types'; export interface PlainAction<T extends string> { type: T; @@ -42,6 +42,8 @@ export enum ActionTypes { HIDE_ERROR = 'HIDE_ERROR', CLEAR_ERROR = 'CLEAR_ERROR', RESET_AMOUNT = 'RESET_AMOUNT', + OPEN_STANDARD_SLIDING_PANEL = 'OPEN_STANDARD_SLIDING_PANEL', + CLOSE_STANDARD_SLIDING_PANEL = 'CLOSE_STANDARD_SLIDING_PANEL', } export const actions = { @@ -68,4 +70,7 @@ export const actions = { hideError: () => createAction(ActionTypes.HIDE_ERROR), clearError: () => createAction(ActionTypes.CLEAR_ERROR), resetAmount: () => createAction(ActionTypes.RESET_AMOUNT), + openStandardSlidingPanel: (content: StandardSlidingPanelContent) => + createAction(ActionTypes.OPEN_STANDARD_SLIDING_PANEL, content), + closeStandardSlidingPanel: () => createAction(ActionTypes.CLOSE_STANDARD_SLIDING_PANEL), }; diff --git a/packages/instant/src/redux/reducer.ts b/packages/instant/src/redux/reducer.ts index a5a1b6f7d..a542979cb 100644 --- a/packages/instant/src/redux/reducer.ts +++ b/packages/instant/src/redux/reducer.ts @@ -19,6 +19,8 @@ import { OrderProcessState, OrderState, ProviderState, + StandardSlidingPanelContent, + StandardSlidingPanelSettings, } from '../types'; import { Action, ActionTypes } from './actions'; @@ -30,6 +32,7 @@ export interface DefaultState { buyOrderState: OrderState; latestErrorDisplayStatus: DisplayStatus; quoteRequestState: AsyncProcessState; + standardSlidingPanelSettings: StandardSlidingPanelSettings; } // State that is required but needs to be derived from the props @@ -56,6 +59,10 @@ export const DEFAULT_STATE: DefaultState = { buyOrderState: { processState: OrderProcessState.None }, latestErrorDisplayStatus: DisplayStatus.Hidden, quoteRequestState: AsyncProcessState.None, + standardSlidingPanelSettings: { + animationState: 'none', + content: StandardSlidingPanelContent.None, + }, }; export const createReducer = (initialState: State) => { @@ -213,6 +220,22 @@ export const createReducer = (initialState: State) => { ...state, availableAssets: action.data, }; + case ActionTypes.OPEN_STANDARD_SLIDING_PANEL: + return { + ...state, + standardSlidingPanelSettings: { + content: action.data, + animationState: 'slidIn', + }, + }; + case ActionTypes.CLOSE_STANDARD_SLIDING_PANEL: + return { + ...state, + standardSlidingPanelSettings: { + content: state.standardSlidingPanelSettings.content, + animationState: 'slidOut', + }, + }; default: return state; } diff --git a/packages/instant/src/types.ts b/packages/instant/src/types.ts index 62afe652d..0377f57d5 100644 --- a/packages/instant/src/types.ts +++ b/packages/instant/src/types.ts @@ -126,3 +126,15 @@ export interface AddressAndEthBalanceInWei { address: string; ethBalanceInWei: BigNumber; } + +export type SlideAnimationState = 'slidIn' | 'slidOut' | 'none'; + +export enum StandardSlidingPanelContent { + None = 'NONE', + InstallWallet = 'INSTALL_WALLET', +} + +export interface StandardSlidingPanelSettings { + animationState: SlideAnimationState; + content: StandardSlidingPanelContent; +} diff --git a/packages/instant/src/util/util.ts b/packages/instant/src/util/util.ts index 232a86850..aeb68956b 100644 --- a/packages/instant/src/util/util.ts +++ b/packages/instant/src/util/util.ts @@ -2,4 +2,5 @@ import * as _ from 'lodash'; export const util = { boundNoop: _.noop.bind(_), + createHrefOnClick: (href: string) => () => window.open(href, '_blank'), }; |