diff options
author | Francesco Agosti <francesco.agosti93@gmail.com> | 2018-11-14 09:31:38 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-11-14 09:31:38 +0800 |
commit | 4fc457b78b30e761164eac26fe5f1ebcddd11f7d (patch) | |
tree | 05a0d01029815d36370f8548d365289f555e8be4 | |
parent | e02dc13805349a770506350c69e9061f596b48b2 (diff) | |
parent | 2f6b1273aaf621beebcbc70af4bb6c5f4a8217a3 (diff) | |
download | dexon-sol-tools-4fc457b78b30e761164eac26fe5f1ebcddd11f7d.tar.gz dexon-sol-tools-4fc457b78b30e761164eac26fe5f1ebcddd11f7d.tar.zst dexon-sol-tools-4fc457b78b30e761164eac26fe5f1ebcddd11f7d.zip |
Merge pull request #1242 from 0xProject/feature/instant/metamask-connect-flow
[instant] Install/Unlock MetaMask, connect PaymentDropdown to redux state
38 files changed, 644 insertions, 153 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..8b6121e43 100644 --- a/packages/instant/src/components/buy_button.tsx +++ b/packages/instant/src/components/buy_button.tsx @@ -43,7 +43,6 @@ export class BuyButton extends React.Component<BuyButtonProps> { onClick={this._handleClick} isDisabled={shouldDisableButton} fontColor={ColorOption.white} - fontSize="20px" > Buy </Button> diff --git a/packages/instant/src/components/buy_order_progress.tsx b/packages/instant/src/components/buy_order_progress.tsx index bc7319423..6568de91b 100644 --- a/packages/instant/src/components/buy_order_progress.tsx +++ b/packages/instant/src/components/buy_order_progress.tsx @@ -12,7 +12,6 @@ export interface BuyOrderProgressProps { export const BuyOrderProgress: React.StatelessComponent<BuyOrderProgressProps> = props => { const { buyOrderState } = props; - if ( buyOrderState.processState === OrderProcessState.Processing || buyOrderState.processState === OrderProcessState.Success || @@ -30,6 +29,5 @@ export const BuyOrderProgress: React.StatelessComponent<BuyOrderProgressProps> = </Container> ); } - return null; }; diff --git a/packages/instant/src/components/buy_order_state_buttons.tsx b/packages/instant/src/components/buy_order_state_buttons.tsx index 6041bf4f5..e563bec73 100644 --- a/packages/instant/src/components/buy_order_state_buttons.tsx +++ b/packages/instant/src/components/buy_order_state_buttons.tsx @@ -35,7 +35,7 @@ export const BuyOrderStateButtons: React.StatelessComponent<BuyOrderStateButtonP if (props.buyOrderProcessingState === OrderProcessState.Failure) { return ( <Flex justify="space-between"> - <Button width="48%" onClick={props.onRetry} fontColor={ColorOption.white} fontSize="16px"> + <Button width="48%" onClick={props.onRetry} fontColor={ColorOption.white}> Back </Button> <SecondaryButton width="48%" onClick={props.onViewTransaction}> 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..546874212 --- /dev/null +++ b/packages/instant/src/components/install_wallet_panel_content.tsx @@ -0,0 +1,32 @@ +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%" + 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..49ec22164 100644 --- a/packages/instant/src/components/payment_method.tsx +++ b/packages/instant/src/components/payment_method.tsx @@ -1,45 +1,132 @@ -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 } 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; + onInstallWalletClick: () => void; + onUnlockWalletClick: () => 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.Locked: + case AccountState.None: + return 'connect your wallet'; + case AccountState.Ready: + 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: + // Just take up the same amount of space as the other states. + return <Container height="52px" />; + case AccountState.Locked: + return ( + <WalletPrompt + onClick={this.props.onUnlockWalletClick} + image={<Icon width={13} icon="lock" color={ColorOption.black} />} + > + Please Unlock MetaMask + </WalletPrompt> + ); + case AccountState.None: + return ( + <WalletPrompt + onClick={this.props.onInstallWalletClick} + image={<MetaMaskLogo width={19} height={18} />} + > + Install MetaMask + </WalletPrompt> + ); + case AccountState.Ready: + return ( + <PaymentMethodDropdown + accountAddress={account.address} + accountEthBalanceInWei={account.ethBalanceInWei} + network={network} + /> + ); + } + }; +} + +interface WalletPromptProps { + image: React.ReactNode; + onClick?: () => void; +} + +const WalletPrompt: React.StatelessComponent<WalletPromptProps> = ({ onClick, image, children }) => ( + <Container + padding="14.5px" + 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/placing_order_button.tsx b/packages/instant/src/components/placing_order_button.tsx index d774d7d27..2516b90b1 100644 --- a/packages/instant/src/components/placing_order_button.tsx +++ b/packages/instant/src/components/placing_order_button.tsx @@ -7,9 +7,9 @@ import { Container } from './ui/container'; import { Spinner } from './ui/spinner'; export const PlacingOrderButton: React.StatelessComponent<{}> = props => ( - <Button isDisabled={true} width="100%" fontColor={ColorOption.white} fontSize="20px"> + <Button isDisabled={true} width="100%" fontColor={ColorOption.white}> <Container display="inline-block" position="relative" top="3px" marginRight="8px"> - <Spinner widthPx={20} heightPx={20} /> + <Spinner widthPx={16} heightPx={16} /> </Container> Placing Order… </Button> diff --git a/packages/instant/src/components/scaling_input.tsx b/packages/instant/src/components/scaling_input.tsx index 1abadb78b..e1599a316 100644 --- a/packages/instant/src/components/scaling_input.tsx +++ b/packages/instant/src/components/scaling_input.tsx @@ -156,8 +156,6 @@ export class ScalingInput extends React.Component<ScalingInputProps, ScalingInpu return `${width}px`; } return `${textLengthThreshold}ch`; - default: - return '1ch'; } }; private readonly _calculateFontSize = (phase: ScalingInputPhase): number => { diff --git a/packages/instant/src/components/secondary_button.tsx b/packages/instant/src/components/secondary_button.tsx index df0539606..705390e28 100644 --- a/packages/instant/src/components/secondary_button.tsx +++ b/packages/instant/src/components/secondary_button.tsx @@ -15,8 +15,6 @@ export const SecondaryButton: React.StatelessComponent<SecondaryButtonProps> = p borderColor={ColorOption.lightGrey} width={props.width} onClick={props.onClick} - fontColor={ColorOption.primaryColor} - fontSize="16px" {...buttonProps} > {props.children} 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..89e4da70c --- /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 SPACING_BETWEEN_PX = '20px'; + +export const StandardPanelContent: React.StatelessComponent<StandardPanelContentProps> = ({ + image, + title, + description, + moreInfoSettings, + action, +}) => ( + <Container height="100%"> + <Flex direction="column" height="calc(100% - 58px)"> + <Container marginBottom={SPACING_BETWEEN_PX}>{image}</Container> + <Container marginBottom={SPACING_BETWEEN_PX}> + <Text fontSize="20px" fontWeight={700} fontColor={ColorOption.black}> + {title} + </Text> + </Container> + <Container marginBottom={SPACING_BETWEEN_PX}> + <Text fontSize="14px" fontColor={ColorOption.grey} center={true}> + {description} + </Text> + </Container> + <Container marginBottom={SPACING_BETWEEN_PX}> + {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..f587ff79a --- /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 />; + case StandardSlidingPanelContent.None: + 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..e77b1b5d1 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.createOpenUrlInNewWindow(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,11 +78,10 @@ export const Button = styled(PlainButton)` Button.defaultProps = { backgroundColor: ColorOption.primaryColor, - borderColor: ColorOption.primaryColor, width: 'auto', isDisabled: false, - padding: '.6em 1.2em', - fontSize: '15px', + padding: '.82em 1.2em', + fontSize: '16px', }; Button.displayName = 'Button'; 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..fd14cc4d1 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.createOpenUrlInNewWindow(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..2267b4dbf 100644 --- a/packages/instant/src/components/zero_ex_instant.tsx +++ b/packages/instant/src/components/zero_ex_instant.tsx @@ -1,8 +1,9 @@ import * as React from 'react'; +import { ZeroExInstantContainer } from '../components/zero_ex_instant_container'; + import { INJECTED_DIV_CLASS } from '../constants'; -import { ZeroExInstantContainer } from './zero_ex_instant_container'; import { ZeroExInstantProvider, ZeroExInstantProviderProps } from './zero_ex_instant_provider'; export type ZeroExInstantProps = ZeroExInstantProviderProps; diff --git a/packages/instant/src/components/zero_ex_instant_container.tsx b/packages/instant/src/components/zero_ex_instant_container.tsx index 5748e064e..c0a197590 100644 --- a/packages/instant/src/components/zero_ex_instant_container.tsx +++ b/packages/instant/src/components/zero_ex_instant_container.tsx @@ -1,26 +1,29 @@ import * as React from 'react'; import { AvailableERC20TokenSelector } from '../containers/available_erc20_token_selector'; +import { ConnectedBuyOrderProgressOrPaymentMethod } from '../containers/connected_buy_order_progress_or_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'; import { SelectedAssetBuyOrderStateButtons } from '../containers/selected_asset_buy_order_state_buttons'; 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; } -export class ZeroExInstantContainer extends React.Component<ZeroExInstantContainerProps, ZeroExInstantContainerState> { +export class ZeroExInstantContainer extends React.Component<{}, ZeroExInstantContainerState> { public state = { tokenSelectionPanelAnimationState: 'none' as SlideAnimationState, }; @@ -47,19 +50,19 @@ export class ZeroExInstantContainer extends React.Component<ZeroExInstantContain > <Flex direction="column" justify="flex-start" height="100%"> <SelectedAssetInstantHeading onSelectAssetClick={this._handleSymbolClick} /> - <SelectedAssetBuyOrderProgress /> + <ConnectedBuyOrderProgressOrPaymentMethod /> <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> diff --git a/packages/instant/src/components/zero_ex_instant_overlay.tsx b/packages/instant/src/components/zero_ex_instant_overlay.tsx index 10438ab7a..2856ea3e3 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 { ZeroExInstantContainer } from '../components/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 { diff --git a/packages/instant/src/components/zero_ex_instant_provider.tsx b/packages/instant/src/components/zero_ex_instant_provider.tsx index e64579518..18e71edb6 100644 --- a/packages/instant/src/components/zero_ex_instant_provider.tsx +++ b/packages/instant/src/components/zero_ex_instant_provider.tsx @@ -91,12 +91,13 @@ export class ZeroExInstantProvider extends React.Component<ZeroExInstantProvider } public componentDidMount(): void { const state = this._store.getState(); + const dispatch = this._store.dispatch; // tslint:disable-next-line:no-floating-promises - asyncData.fetchEthPriceAndDispatchToStore(this._store); + asyncData.fetchEthPriceAndDispatchToStore(dispatch); // fetch available assets if none are specified if (_.isUndefined(state.availableAssets)) { // tslint:disable-next-line:no-floating-promises - asyncData.fetchAvailableAssetDatasAndDispatchToStore(this._store); + asyncData.fetchAvailableAssetDatasAndDispatchToStore(state, dispatch); } if (state.providerState.account.state !== AccountState.None) { this._accountUpdateHeartbeat = generateAccountHeartbeater({ @@ -112,7 +113,7 @@ export class ZeroExInstantProvider extends React.Component<ZeroExInstantProvider }); this._buyQuoteHeartbeat.start(BUY_QUOTE_UPDATE_INTERVAL_TIME_MS); // tslint:disable-next-line:no-floating-promises - asyncData.fetchCurrentBuyQuoteAndDispatchToStore({ store: this._store, shouldSetPending: true }); + asyncData.fetchCurrentBuyQuoteAndDispatchToStore(state, dispatch, true); // warm up the gas price estimator cache just in case we can't // grab the gas price estimate when submitting the transaction // tslint:disable-next-line:no-floating-promises diff --git a/packages/instant/src/constants.ts b/packages/instant/src/constants.ts index 110a8248a..2bf7849ec 100644 --- a/packages/instant/src/constants.ts +++ b/packages/instant/src/constants.ts @@ -19,6 +19,9 @@ 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 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..65b3710a6 --- /dev/null +++ b/packages/instant/src/containers/connected_account_payment_method.ts @@ -0,0 +1,59 @@ +import * as React from 'react'; +import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; + +import { PaymentMethod, PaymentMethodProps } from '../components/payment_method'; +import { Action, actions } from '../redux/actions'; +import { asyncData } from '../redux/async_data'; +import { State } from '../redux/reducer'; +import { Network, Omit, ProviderState, StandardSlidingPanelContent } from '../types'; + +export interface ConnectedAccountPaymentMethodProps {} + +interface ConnectedState { + network: Network; + providerState: ProviderState; +} + +interface ConnectedDispatch { + onInstallWalletClick: () => void; + unlockWalletAndDispatchToStore: (providerState: ProviderState) => void; +} + +type ConnectedProps = Omit<PaymentMethodProps, keyof ConnectedAccountPaymentMethodProps>; + +type FinalProps = ConnectedProps & ConnectedAccountPaymentMethodProps; + +const mapStateToProps = (state: State, _ownProps: ConnectedAccountPaymentMethodProps): ConnectedState => ({ + network: state.network, + providerState: state.providerState, +}); + +const mapDispatchToProps = ( + dispatch: Dispatch<Action>, + ownProps: ConnectedAccountPaymentMethodProps, +): ConnectedDispatch => ({ + onInstallWalletClick: () => dispatch(actions.openStandardSlidingPanel(StandardSlidingPanelContent.InstallWallet)), + unlockWalletAndDispatchToStore: async (providerState: ProviderState) => + asyncData.fetchAccountInfoAndDispatchToStore(providerState, dispatch, true), +}); + +const mergeProps = ( + connectedState: ConnectedState, + connectedDispatch: ConnectedDispatch, + ownProps: ConnectedAccountPaymentMethodProps, +): FinalProps => ({ + ...ownProps, + network: connectedState.network, + account: connectedState.providerState.account, + onInstallWalletClick: connectedDispatch.onInstallWalletClick, + onUnlockWalletClick: () => { + connectedDispatch.unlockWalletAndDispatchToStore(connectedState.providerState); + }, +}); + +export const ConnectedAccountPaymentMethod: React.ComponentClass<ConnectedAccountPaymentMethodProps> = connect( + mapStateToProps, + mapDispatchToProps, + mergeProps, +)(PaymentMethod); diff --git a/packages/instant/src/containers/connected_buy_order_progress_or_payment_method.tsx b/packages/instant/src/containers/connected_buy_order_progress_or_payment_method.tsx new file mode 100644 index 000000000..05071c8c3 --- /dev/null +++ b/packages/instant/src/containers/connected_buy_order_progress_or_payment_method.tsx @@ -0,0 +1,36 @@ +import * as React from 'react'; +import { connect } from 'react-redux'; + +import { State } from '../redux/reducer'; +import { OrderProcessState } from '../types'; + +import { ConnectedAccountPaymentMethod } from './connected_account_payment_method'; +import { SelectedAssetBuyOrderProgress } from './selected_asset_buy_order_progress'; + +interface BuyOrderProgressOrPaymentMethodProps { + orderProcessState: OrderProcessState; +} +export const BuyOrderProgressOrPaymentMethod = (props: BuyOrderProgressOrPaymentMethodProps) => { + const { orderProcessState } = props; + if ( + orderProcessState === OrderProcessState.Processing || + orderProcessState === OrderProcessState.Success || + orderProcessState === OrderProcessState.Failure + ) { + return <SelectedAssetBuyOrderProgress />; + } + if (orderProcessState === OrderProcessState.None) { + return <ConnectedAccountPaymentMethod />; + } + return null; +}; + +interface ConnectedState extends BuyOrderProgressOrPaymentMethodProps {} + +export interface ConnectedBuyOrderProgressOrPaymentMethodProps {} +const mapStateToProps = (state: State, _ownProps: ConnectedBuyOrderProgressOrPaymentMethodProps): ConnectedState => ({ + orderProcessState: state.buyOrderState.processState, +}); +export const ConnectedBuyOrderProgressOrPaymentMethod: React.ComponentClass< + ConnectedBuyOrderProgressOrPaymentMethodProps +> = connect(mapStateToProps)(BuyOrderProgressOrPaymentMethod); 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/containers/selected_erc20_asset_amount_input.ts b/packages/instant/src/containers/selected_erc20_asset_amount_input.ts index 7dd0d874e..8b0070228 100644 --- a/packages/instant/src/containers/selected_erc20_asset_amount_input.ts +++ b/packages/instant/src/containers/selected_erc20_asset_amount_input.ts @@ -6,11 +6,11 @@ import * as React from 'react'; import { connect } from 'react-redux'; import { Dispatch } from 'redux'; -import { ERC20AssetAmountInput } from '../components/erc20_asset_amount_input'; +import { ERC20AssetAmountInput, ERC20AssetAmountInputProps } from '../components/erc20_asset_amount_input'; import { Action, actions } from '../redux/actions'; import { State } from '../redux/reducer'; import { ColorOption } from '../style/theme'; -import { AffiliateInfo, ERC20Asset, OrderProcessState } from '../types'; +import { AffiliateInfo, ERC20Asset, Omit, OrderProcessState } from '../types'; import { buyQuoteUpdater } from '../util/buy_quote_updater'; export interface SelectedERC20AssetAmountInputProps { @@ -37,13 +37,7 @@ interface ConnectedDispatch { ) => void; } -interface ConnectedProps { - value?: BigNumber; - asset?: ERC20Asset; - onChange: (value?: BigNumber, asset?: ERC20Asset) => void; - isDisabled: boolean; - numberOfAssetsAvailable?: number; -} +type ConnectedProps = Omit<ERC20AssetAmountInputProps, keyof SelectedERC20AssetAmountInputProps>; type FinalProps = ConnectedProps & SelectedERC20AssetAmountInputProps; diff --git a/packages/instant/src/redux/actions.ts b/packages/instant/src/redux/actions.ts index ba50fab7a..77e3dec12 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; @@ -41,6 +41,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 = { @@ -67,4 +69,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/async_data.ts b/packages/instant/src/redux/async_data.ts index 3f5c06974..a1952e429 100644 --- a/packages/instant/src/redux/async_data.ts +++ b/packages/instant/src/redux/async_data.ts @@ -1,94 +1,90 @@ import { AssetProxyId } from '@0x/types'; +import { Web3Wrapper } from '@0x/web3-wrapper'; import * as _ from 'lodash'; +import { Dispatch } from 'redux'; import { BIG_NUMBER_ZERO } from '../constants'; -import { AccountState, ERC20Asset, OrderProcessState } from '../types'; +import { AccountState, ERC20Asset, OrderProcessState, ProviderState } from '../types'; import { assetUtils } from '../util/asset'; import { buyQuoteUpdater } from '../util/buy_quote_updater'; import { coinbaseApi } from '../util/coinbase_api'; import { errorFlasher } from '../util/error_flasher'; import { actions } from './actions'; -import { Store } from './store'; +import { State } from './reducer'; export const asyncData = { - fetchEthPriceAndDispatchToStore: async (store: Store) => { + fetchEthPriceAndDispatchToStore: async (dispatch: Dispatch) => { try { const ethUsdPrice = await coinbaseApi.getEthUsdPrice(); - store.dispatch(actions.updateEthUsdPrice(ethUsdPrice)); + dispatch(actions.updateEthUsdPrice(ethUsdPrice)); } catch (e) { const errorMessage = 'Error fetching ETH/USD price'; - errorFlasher.flashNewErrorMessage(store.dispatch, errorMessage); - store.dispatch(actions.updateEthUsdPrice(BIG_NUMBER_ZERO)); + errorFlasher.flashNewErrorMessage(dispatch, errorMessage); + dispatch(actions.updateEthUsdPrice(BIG_NUMBER_ZERO)); } }, - fetchAvailableAssetDatasAndDispatchToStore: async (store: Store) => { - const { providerState, assetMetaDataMap, network } = store.getState(); + fetchAvailableAssetDatasAndDispatchToStore: async (state: State, dispatch: Dispatch) => { + const { providerState, assetMetaDataMap, network } = state; const assetBuyer = providerState.assetBuyer; try { const assetDatas = await assetBuyer.getAvailableAssetDatasAsync(); const assets = assetUtils.createAssetsFromAssetDatas(assetDatas, assetMetaDataMap, network); - store.dispatch(actions.setAvailableAssets(assets)); + dispatch(actions.setAvailableAssets(assets)); } catch (e) { const errorMessage = 'Could not find any assets'; - errorFlasher.flashNewErrorMessage(store.dispatch, errorMessage); + errorFlasher.flashNewErrorMessage(dispatch, errorMessage); // On error, just specify that none are available - store.dispatch(actions.setAvailableAssets([])); + dispatch(actions.setAvailableAssets([])); } }, - fetchAccountInfoAndDispatchToStore: async (options: { store: Store; shouldSetToLoading: boolean }) => { - const { store, shouldSetToLoading } = options; - const { providerState } = store.getState(); + fetchAccountInfoAndDispatchToStore: async ( + providerState: ProviderState, + dispatch: Dispatch, + shouldAttemptUnlock: boolean = false, + shouldSetToLoading: boolean = false, + ) => { const web3Wrapper = providerState.web3Wrapper; const provider = providerState.provider; if (shouldSetToLoading && providerState.account.state !== AccountState.Loading) { - store.dispatch(actions.setAccountStateLoading()); + dispatch(actions.setAccountStateLoading()); } let availableAddresses: string[]; try { // TODO(bmillman): Add support at the web3Wrapper level for calling `eth_requestAccounts` instead of calling enable here const isPrivacyModeEnabled = !_.isUndefined((provider as any).enable); - availableAddresses = isPrivacyModeEnabled - ? await (provider as any).enable() - : await web3Wrapper.getAvailableAddressesAsync(); + availableAddresses = + isPrivacyModeEnabled && shouldAttemptUnlock + ? await (provider as any).enable() + : await web3Wrapper.getAvailableAddressesAsync(); } catch (e) { - store.dispatch(actions.setAccountStateLocked()); + dispatch(actions.setAccountStateLocked()); return; } if (!_.isEmpty(availableAddresses)) { const activeAddress = availableAddresses[0]; - store.dispatch(actions.setAccountStateReady(activeAddress)); + dispatch(actions.setAccountStateReady(activeAddress)); // tslint:disable-next-line:no-floating-promises - asyncData.fetchAccountBalanceAndDispatchToStore(store); + asyncData.fetchAccountBalanceAndDispatchToStore(activeAddress, providerState.web3Wrapper, dispatch); } else { - store.dispatch(actions.setAccountStateLocked()); + 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; - } + fetchAccountBalanceAndDispatchToStore: async (address: string, web3Wrapper: Web3Wrapper, dispatch: Dispatch) => { try { - const address = account.address; const ethBalanceInWei = await web3Wrapper.getBalanceInWeiAsync(address); - store.dispatch(actions.updateAccountEthBalance({ address, ethBalanceInWei })); + dispatch(actions.updateAccountEthBalance({ address, ethBalanceInWei })); } catch (e) { // leave balance as is return; } }, - fetchCurrentBuyQuoteAndDispatchToStore: async (options: { store: Store; shouldSetPending: boolean }) => { - const { store, shouldSetPending } = options; - const { - buyOrderState, - providerState, - selectedAsset, - selectedAssetUnitAmount, - affiliateInfo, - } = store.getState(); + fetchCurrentBuyQuoteAndDispatchToStore: async ( + state: State, + dispatch: Dispatch, + shouldSetPending: boolean = false, + ) => { + const { buyOrderState, providerState, selectedAsset, selectedAssetUnitAmount, affiliateInfo } = state; const assetBuyer = providerState.assetBuyer; if ( !_.isUndefined(selectedAssetUnitAmount) && @@ -98,7 +94,7 @@ export const asyncData = { ) { await buyQuoteUpdater.updateBuyQuoteAsync( assetBuyer, - store.dispatch, + dispatch, selectedAsset as ERC20Asset, selectedAssetUnitAmount, shouldSetPending, diff --git a/packages/instant/src/redux/reducer.ts b/packages/instant/src/redux/reducer.ts index 77c99627a..dfc2b89f3 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) => { @@ -66,11 +73,19 @@ export const createReducer = (initialState: State) => { case ActionTypes.SET_ACCOUNT_STATE_LOCKED: return reduceStateWithAccount(state, LOCKED_ACCOUNT); case ActionTypes.SET_ACCOUNT_STATE_READY: { - const account: AccountReady = { + const address = action.data; + let newAccount: AccountReady = { state: AccountState.Ready, - address: action.data, + address, }; - return reduceStateWithAccount(state, account); + const currentAccount = state.providerState.account; + if (currentAccount.state === AccountState.Ready && currentAccount.address === address) { + newAccount = { + ...newAccount, + ethBalanceInWei: currentAccount.ethBalanceInWei, + }; + } + return reduceStateWithAccount(state, newAccount); } case ActionTypes.UPDATE_ACCOUNT_ETH_BALANCE: { const { address, ethBalanceInWei } = action.data; @@ -211,6 +226,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 b43a82d46..b6f449f38 100644 --- a/packages/instant/src/types.ts +++ b/packages/instant/src/types.ts @@ -125,3 +125,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/asset.ts b/packages/instant/src/util/asset.ts index fbfbb19f3..40560d3eb 100644 --- a/packages/instant/src/util/asset.ts +++ b/packages/instant/src/util/asset.ts @@ -80,8 +80,6 @@ export const assetUtils = { return metaData.symbol.toUpperCase(); case AssetProxyId.ERC721: return metaData.name; - default: - return defaultName; } }, formattedSymbolForAsset: (asset?: ERC20Asset, defaultName: string = '???'): string => { diff --git a/packages/instant/src/util/etherscan.ts b/packages/instant/src/util/etherscan.ts index 4d62c4d9f..f9bf82827 100644 --- a/packages/instant/src/util/etherscan.ts +++ b/packages/instant/src/util/etherscan.ts @@ -8,9 +8,8 @@ const etherscanPrefix = (networkId: number): string | undefined => { return 'kovan.'; case Network.Mainnet: return ''; - default: - return undefined; } + return ''; }; export const etherscanUtil = { diff --git a/packages/instant/src/util/heartbeater_factory.ts b/packages/instant/src/util/heartbeater_factory.ts index 96a8ac4e6..06fcdb8bb 100644 --- a/packages/instant/src/util/heartbeater_factory.ts +++ b/packages/instant/src/util/heartbeater_factory.ts @@ -10,13 +10,13 @@ export interface HeartbeatFactoryOptions { export const generateAccountHeartbeater = (options: HeartbeatFactoryOptions): Heartbeater => { const { store, shouldPerformImmediatelyOnStart } = options; return new Heartbeater(async () => { - await asyncData.fetchAccountInfoAndDispatchToStore({ store, shouldSetToLoading: false }); + await asyncData.fetchAccountInfoAndDispatchToStore(store.getState().providerState, store.dispatch, false); }, shouldPerformImmediatelyOnStart); }; export const generateBuyQuoteHeartbeater = (options: HeartbeatFactoryOptions): Heartbeater => { const { store, shouldPerformImmediatelyOnStart } = options; return new Heartbeater(async () => { - await asyncData.fetchCurrentBuyQuoteAndDispatchToStore({ store, shouldSetPending: false }); + await asyncData.fetchCurrentBuyQuoteAndDispatchToStore(store.getState(), store.dispatch, false); }, shouldPerformImmediatelyOnStart); }; diff --git a/packages/instant/src/util/util.ts b/packages/instant/src/util/util.ts index 232a86850..29b6b1d2b 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(_), + createOpenUrlInNewWindow: (href: string) => () => window.open(href, '_blank'), }; diff --git a/packages/instant/tslint.json b/packages/instant/tslint.json index 08b76be97..d43ee8da7 100644 --- a/packages/instant/tslint.json +++ b/packages/instant/tslint.json @@ -3,6 +3,7 @@ "rules": { "custom-no-magic-numbers": false, "semicolon": [true, "always", "ignore-bound-class-methods"], - "max-classes-per-file": false + "max-classes-per-file": false, + "switch-default": false } } |