diff options
Diffstat (limited to 'packages/website')
19 files changed, 444 insertions, 111 deletions
diff --git a/packages/website/package.json b/packages/website/package.json index 69115be9a..95804f988 100644 --- a/packages/website/package.json +++ b/packages/website/package.json @@ -11,22 +11,19 @@ "clean": "shx rm -f public/bundle*", "lint": "tslint --project . 'ts/**/*.ts' 'ts/**/*.tsx'", "watch": "webpack-dev-server --content-base public --https", - "deploy_dogfood": - "npm run build; aws s3 sync ./public/. s3://dogfood.0xproject.com --profile 0xproject --region us-east-1 --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers", - "deploy_staging": - "npm run build; aws s3 sync ./public/. s3://staging-0xproject --profile 0xproject --region us-east-1 --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers", - "deploy_live": - "npm run build; aws s3 sync ./public/. s3://0xproject.com --profile 0xproject --region us-east-1 --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers" + "deploy_dogfood": "npm run build; aws s3 sync ./public/. s3://dogfood.0xproject.com --profile 0xproject --region us-east-1 --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers", + "deploy_staging": "npm run build; aws s3 sync ./public/. s3://staging-0xproject --profile 0xproject --region us-east-1 --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers", + "deploy_live": "npm run build; aws s3 sync ./public/. s3://0xproject.com --profile 0xproject --region us-east-1 --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers" }, "author": "Fabio Berger", "license": "Apache-2.0", "dependencies": { + "@0xproject/contract-wrappers": "^0.0.2", "@0xproject/react-docs": "^0.0.12", "@0xproject/react-shared": "^0.1.7", "@0xproject/subproviders": "^0.10.2", - "@0xproject/contract-wrappers": "^0.0.2", - "@0xproject/typescript-typings": "^0.3.2", "@0xproject/types": "0.7.0", + "@0xproject/typescript-typings": "^0.3.2", "@0xproject/utils": "^0.6.2", "@0xproject/web3-wrapper": "^0.6.4", "accounting": "^0.4.1", @@ -46,7 +43,7 @@ "react-document-title": "^2.0.3", "react-dom": "15.6.1", "react-ga": "^2.4.1", - "react-joyride": "^2.0.0-11", + "react-popper": "^1.0.0-beta.6", "react-redux": "^5.0.3", "react-router-dom": "^4.1.1", "react-scroll": "^1.5.2", diff --git a/packages/website/public/index.html b/packages/website/public/index.html index c28e4abf4..4c0985c71 100644 --- a/packages/website/public/index.html +++ b/packages/website/public/index.html @@ -26,53 +26,53 @@ <!-- Global site tag (gtag.js) - Google Analytics --> <script async src="https://www.googletagmanager.com/gtag/js?id=UA-98720122-1"></script> <script> - window.dataLayer = window.dataLayer || []; -function gtag() { - dataLayer.push(arguments); -} -gtag('js', new Date()); + window.dataLayer = window.dataLayer || []; + function gtag() { + dataLayer.push(arguments); + } + gtag('js', new Date()); -gtag('config', 'UA-98720122-1'); -</script> + gtag('config', 'UA-98720122-1'); + </script> <!-- End Google Analytics --> <!-- Facebook SDK --> <div id="fb-root"></div> <script> - (function(d, s, id) { - var js, - fjs = d.getElementsByTagName(s)[0]; - if (d.getElementById(id)) return; - js = d.createElement(s); - js.id = id; - js.src = '//connect.facebook.net/en_US/sdk.js#xfbml=1&version=v2.8&appId=1687545238205192'; - fjs.parentNode.insertBefore(js, fjs); -})(document, 'script', 'facebook-jssdk'); -</script> + (function (d, s, id) { + var js, + fjs = d.getElementsByTagName(s)[0]; + if (d.getElementById(id)) return; + js = d.createElement(s); + js.id = id; + js.src = '//connect.facebook.net/en_US/sdk.js#xfbml=1&version=v2.8&appId=1687545238205192'; + fjs.parentNode.insertBefore(js, fjs); + })(document, 'script', 'facebook-jssdk'); + </script> <div id="app"></div> <!-- End Facebook SDK --> <!-- Twitter SDK --> <script> - window.twttr = (function(d, s, id) { - var js, - fjs = d.getElementsByTagName(s)[0], - t = window.twttr || {}; - if (d.getElementById(id)) return t; - js = d.createElement(s); - js.id = id; - js.src = 'https://platform.twitter.com/widgets.js'; - fjs.parentNode.insertBefore(js, fjs); + window.twttr = (function (d, s, id) { + var js, + fjs = d.getElementsByTagName(s)[0], + t = window.twttr || {}; + if (d.getElementById(id)) return t; + js = d.createElement(s); + js.id = id; + js.src = 'https://platform.twitter.com/widgets.js'; + fjs.parentNode.insertBefore(js, fjs); - t._e = []; - t.ready = function(f) { - t._e.push(f); - }; - return t; -})(document, 'script', 'twitter-wjs'); -</script> + t._e = []; + t.ready = function (f) { + t._e.push(f); + }; + return t; + })(document, 'script', 'twitter-wjs'); + </script> <!-- End Twitter SDK --> <!-- Main --> <script type="text/javascript" crossorigin="anonymous" src="/bundle.js" charset="utf-8"></script> </body> -</html> +</html>
\ No newline at end of file diff --git a/packages/website/ts/components/onboarding/onboarding_flow.tsx b/packages/website/ts/components/onboarding/onboarding_flow.tsx index 621d14260..4066babaf 100644 --- a/packages/website/ts/components/onboarding/onboarding_flow.tsx +++ b/packages/website/ts/components/onboarding/onboarding_flow.tsx @@ -1,39 +1,96 @@ import * as _ from 'lodash'; import * as React from 'react'; -import Joyride, { CallbackData, Step, StyleOptions } from 'react-joyride'; +import { Placement, Popper, PopperChildrenProps } from 'react-popper'; +import { ContinueButtonDisplay, OnboardingTooltip } from 'ts/components/onboarding/onboarding_tooltip'; +import { Container } from 'ts/components/ui/container'; +import { Overlay } from 'ts/components/ui/overlay'; import { zIndex } from 'ts/utils/style'; +export interface Step { + target: string; + title?: string; + content: React.ReactNode; + placement?: Placement; + hideBackButton?: boolean; + hideNextButton?: boolean; + continueButtonDisplay?: ContinueButtonDisplay; +} + export interface OnboardingFlowProps { steps: Step[]; stepIndex: number; isRunning: boolean; onClose: () => void; + updateOnboardingStep: (stepIndex: number) => void; } -const joyrideStyleOptions: StyleOptions = { - zIndex: zIndex.overlay, -}; - -// Wrapper around Joyride with defaults and styles set export class OnboardingFlow extends React.Component<OnboardingFlowProps> { public render(): React.ReactNode { + if (!this.props.isRunning) { + return null; + } + return ( + <Overlay> + <Popper referenceElement={this._getElementForStep()} placement={this._getCurrentStep().placement}> + {this._renderPopperChildren.bind(this)} + </Popper> + </Overlay> + ); + } + + private _getElementForStep(): Element { + return document.querySelector(this._getCurrentStep().target); + } + + private _renderPopperChildren(props: PopperChildrenProps): React.ReactNode { return ( - <Joyride - run={this.props.isRunning} - debug={true} - steps={this.props.steps} - stepIndex={this.props.stepIndex} - styles={{ options: joyrideStyleOptions }} - callback={this._handleChange.bind(this)} - /> + <div ref={props.ref} style={props.style} data-placement={props.placement}> + {this._renderToolTip()} + </div> ); } - private _handleChange(data: CallbackData): void { - switch (data.action) { - case 'close': - this.props.onClose(); + private _renderToolTip(): React.ReactNode { + const { steps, stepIndex } = this.props; + const step = steps[stepIndex]; + const isLastStep = steps.length - 1 === stepIndex; + return ( + <Container marginLeft="15px"> + <OnboardingTooltip + title={step.title} + content={step.content} + isLastStep={isLastStep} + hideBackButton={step.hideBackButton} + hideNextButton={step.hideNextButton} + onClose={this.props.onClose} + onClickNext={this._goToNextStep.bind(this)} + onClickBack={this._goToPrevStep.bind(this)} + continueButtonDisplay={step.continueButtonDisplay} + /> + </Container> + ); + } + + private _getCurrentStep(): Step { + return this.props.steps[this.props.stepIndex]; + } + + private _goToNextStep(): void { + const nextStep = this.props.stepIndex + 1; + if (nextStep < this.props.steps.length) { + this.props.updateOnboardingStep(nextStep); + } else { + this.props.onClose(); + } + } + + private _goToPrevStep(): void { + const nextStep = this.props.stepIndex - 1; + if (nextStep >= 0) { + this.props.updateOnboardingStep(nextStep); + } else { + this.props.onClose(); } } } diff --git a/packages/website/ts/components/onboarding/onboarding_tooltip.tsx b/packages/website/ts/components/onboarding/onboarding_tooltip.tsx new file mode 100644 index 000000000..eb34a87f2 --- /dev/null +++ b/packages/website/ts/components/onboarding/onboarding_tooltip.tsx @@ -0,0 +1,56 @@ +import * as React from 'react'; + +import { colors } from '@0xproject/react-shared'; +import { Container } from 'ts/components/ui/container'; +import { Island } from 'ts/components/ui/island'; + +export type ContinueButtonDisplay = 'enabled' | 'disabled'; + +export interface OnboardingTooltipProps { + title?: string; + content: React.ReactNode; + isLastStep: boolean; + onClose: () => void; + onClickNext: () => void; + onClickBack: () => void; + continueButtonDisplay?: ContinueButtonDisplay; + hideBackButton?: boolean; + hideNextButton?: boolean; +} + +// TODO: Make this more general button. +export interface ContinueButtonProps { + display: ContinueButtonDisplay; + children?: string; + onClick: () => void; +} + +export const ContinueButton: React.StatelessComponent<ContinueButtonProps> = (props: ContinueButtonProps) => { + const isDisabled = props.display === 'disabled'; + return ( + <button disabled={isDisabled} onClick={isDisabled ? undefined : props.onClick}> + {props.children} + </button> + ); +}; + +export const OnboardingTooltip: React.StatelessComponent<OnboardingTooltipProps> = (props: OnboardingTooltipProps) => ( + <Island> + <Container paddingRight="30px" paddingLeft="30px" maxWidth={350} paddingTop="15px" paddingBottom="15px"> + <div className="flex flex-column"> + {props.title} + {props.content} + {props.continueButtonDisplay && ( + <ContinueButton onClick={props.onClickNext} display={props.continueButtonDisplay}> + Continue + </ContinueButton> + )} + {!props.hideBackButton && <button onClick={props.onClickBack}>Back</button>} + {!props.hideNextButton && <button onClick={props.onClickNext}>Skip</button>} + <button onClick={props.onClose}>Close</button> + </div> + </Container> + </Island> +); + +OnboardingTooltip.displayName = 'OnboardingTooltip'; diff --git a/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx b/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx index 11684aaee..edaeb3736 100644 --- a/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx +++ b/packages/website/ts/components/onboarding/portal_onboarding_flow.tsx @@ -1,32 +1,123 @@ +import * as _ from 'lodash'; import * as React from 'react'; -import { Step } from 'react-joyride'; -import { OnboardingFlow } from 'ts/components/onboarding/onboarding_flow'; +import { BigNumber } from '@0xproject/utils'; +import { OnboardingFlow, Step } from 'ts/components/onboarding/onboarding_flow'; +import { ProviderType, TokenByAddress } from 'ts/types'; +import { utils } from 'ts/utils/utils'; export interface PortalOnboardingFlowProps { stepIndex: number; isRunning: boolean; - onClose: () => void; + userAddress: string; + hasBeenSeen: boolean; + providerType: ProviderType; + injectedProviderName: string; + blockchainIsLoaded: boolean; + userEthBalanceInWei: BigNumber; + tokenByAddress: TokenByAddress; + updateIsRunning: (isRunning: boolean) => void; + updateOnboardingStep: (stepIndex: number) => void; } -const steps: Step[] = [ - { - target: '.wallet', - content: 'You are onboarding right now!', - placement: 'right', - disableBeacon: true, - }, -]; - export class PortalOnboardingFlow extends React.Component<PortalOnboardingFlowProps> { + public componentDidMount(): void { + this._overrideOnboardingStateIfShould(); + } + public componentDidUpdate(): void { + this._overrideOnboardingStateIfShould(); + } public render(): React.ReactNode { return ( <OnboardingFlow - steps={steps} + steps={this._getSteps()} stepIndex={this.props.stepIndex} isRunning={this.props.isRunning} - onClose={this.props.onClose} + onClose={this.props.updateIsRunning.bind(this, false)} + updateOnboardingStep={this.props.updateOnboardingStep} /> ); } + + private _getSteps(): Step[] { + const steps: Step[] = [ + { + target: '.wallet', + content: + 'Before you begin, you need to connect to a wallet. This will be used across all 0x relayers and dApps', + placement: 'right', + hideBackButton: true, + hideNextButton: true, + }, + { + target: '.wallet', + content: 'Unlock your metamask extension to begin', + placement: 'right', + hideBackButton: true, + hideNextButton: true, + }, + { + target: '.wallet', + content: + 'In order to start trading on any 0x relayer in the 0x ecosystem, you need to complete two simple steps', + placement: 'right', + hideBackButton: true, + continueButtonDisplay: 'enabled', + }, + { + target: '.eth-row', + content: 'Before you begin you will need to send some ETH to your metamask wallet', + placement: 'right', + continueButtonDisplay: this._userHasEth() ? 'enabled' : 'disabled', + }, + { + target: '.weth-row', + content: 'You need to convert some of your ETH into tradeable Wrapped ETH (WETH)', + placement: 'right', + continueButtonDisplay: this._userHasWeth() ? 'enabled' : 'disabled', + }, + ]; + return steps; + } + + private _isAddressAvailable(): boolean { + return !_.isEmpty(this.props.userAddress); + } + + private _userHasEth(): boolean { + return this.props.userEthBalanceInWei > new BigNumber(0); + } + + private _userHasWeth(): boolean { + // TODO: https://app.asana.com/0/681385331277907/690722374136933 + return false; + } + + private _overrideOnboardingStateIfShould(): void { + this._autoStartOnboardingIfShould(); + this._adjustStepIfShould(); + } + + private _adjustStepIfShould(): void { + if (this._isAddressAvailable()) { + if (this.props.stepIndex < 2) { + this.props.updateOnboardingStep(2); + } + return; + } + const isExternallyInjected = utils.isExternallyInjected( + this.props.providerType, + this.props.injectedProviderName, + ); + if (isExternallyInjected) { + this.props.updateOnboardingStep(1); + return; + } + this.props.updateOnboardingStep(0); + } + private _autoStartOnboardingIfShould(): void { + if (!this.props.isRunning && !this.props.hasBeenSeen && this.props.blockchainIsLoaded) { + this.props.updateIsRunning(true); + } + } } diff --git a/packages/website/ts/components/top_bar/provider_display.tsx b/packages/website/ts/components/top_bar/provider_display.tsx index fc516882a..679ec07dc 100644 --- a/packages/website/ts/components/top_bar/provider_display.tsx +++ b/packages/website/ts/components/top_bar/provider_display.tsx @@ -11,6 +11,7 @@ import { Dispatcher } from 'ts/redux/dispatcher'; import { ProviderType } from 'ts/types'; import { colors } from 'ts/utils/colors'; import { constants } from 'ts/utils/constants'; +import { zIndex } from 'ts/utils/style'; import { utils } from 'ts/utils/utils'; const ROOT_HEIGHT = 24; @@ -39,8 +40,10 @@ const styles: Styles = { export class ProviderDisplay extends React.Component<ProviderDisplayProps, ProviderDisplayState> { public render(): React.ReactNode { const isAddressAvailable = !_.isEmpty(this.props.userAddress); - const isExternallyInjectedProvider = - this.props.providerType === ProviderType.Injected && this.props.injectedProviderName !== '0x Public'; + const isExternallyInjectedProvider = utils.isExternallyInjected( + this.props.providerType, + this.props.injectedProviderName, + ); const displayAddress = isAddressAvailable ? utils.getAddressBeginAndEnd(this.props.userAddress) : isExternallyInjectedProvider @@ -69,15 +72,13 @@ export class ProviderDisplay extends React.Component<ProviderDisplayProps, Provi )} </div> ); - const hasInjectedProvider = - this.props.injectedProviderName !== '0x Public' && this.props.providerType === ProviderType.Injected; const hasLedgerProvider = this.props.providerType === ProviderType.Ledger; - const horizontalPosition = hasInjectedProvider || hasLedgerProvider ? 'left' : 'middle'; + const horizontalPosition = isExternallyInjectedProvider || hasLedgerProvider ? 'left' : 'middle'; return ( <div style={{ width: 'fit-content', height: 48, float: 'right' }}> <DropDown hoverActiveNode={hoverActiveNode} - popoverContent={this.renderPopoverContent(hasInjectedProvider, hasLedgerProvider)} + popoverContent={this.renderPopoverContent(isExternallyInjectedProvider, hasLedgerProvider)} anchorOrigin={{ horizontal: horizontalPosition, vertical: 'bottom' }} targetOrigin={{ horizontal: horizontalPosition, vertical: 'top' }} zDepth={1} diff --git a/packages/website/ts/components/ui/container.tsx b/packages/website/ts/components/ui/container.tsx index 07868608c..d577447b0 100644 --- a/packages/website/ts/components/ui/container.tsx +++ b/packages/website/ts/components/ui/container.tsx @@ -1,10 +1,17 @@ import * as React from 'react'; +type StringOrNum = string | number; + export interface ContainerProps { - marginTop?: string | number; - marginBottom?: string | number; - marginRight?: string | number; - marginLeft?: string | number; + marginTop?: StringOrNum; + marginBottom?: StringOrNum; + marginRight?: StringOrNum; + marginLeft?: StringOrNum; + paddingTop?: StringOrNum; + paddingBottom?: StringOrNum; + paddingRight?: StringOrNum; + paddingLeft?: StringOrNum; + maxWidth?: StringOrNum; children?: React.ReactNode; } diff --git a/packages/website/ts/components/ui/overlay.tsx b/packages/website/ts/components/ui/overlay.tsx new file mode 100644 index 000000000..bb2ed8e59 --- /dev/null +++ b/packages/website/ts/components/ui/overlay.tsx @@ -0,0 +1,34 @@ +import { colors } from '@0xproject/react-shared'; +import * as _ from 'lodash'; +import * as React from 'react'; + +import { zIndex } from 'ts/utils/style'; + +export interface OverlayProps { + children?: React.ReactNode; + style?: React.CSSProperties; + onClick?: () => void; +} + +const style: React.CSSProperties = { + position: 'fixed', + top: 0, + right: 0, + bottom: 0, + left: 0, + zIndex: zIndex.overlay, + backgroundColor: 'rgba(0, 0, 0, 0.6)', +}; + +export const Overlay: React.StatelessComponent = (props: OverlayProps) => ( + <div style={{ ...style, ...props.style }} onClick={props.onClick}> + {props.children} + </div> +); + +Overlay.defaultProps = { + style: {}, + onClick: _.noop, +}; + +Overlay.displayName = 'Overlay'; diff --git a/packages/website/ts/components/wallet/wallet.tsx b/packages/website/ts/components/wallet/wallet.tsx index 10ee31619..30d1285f4 100644 --- a/packages/website/ts/components/wallet/wallet.tsx +++ b/packages/website/ts/components/wallet/wallet.tsx @@ -46,6 +46,7 @@ import { import { backendClient } from 'ts/utils/backend_client'; import { colors } from 'ts/utils/colors'; import { constants } from 'ts/utils/constants'; +import { zIndex } from 'ts/utils/style'; import { utils } from 'ts/utils/utils'; import { styles as walletItemStyles } from 'ts/utils/wallet_item_styles'; @@ -88,6 +89,8 @@ interface AccessoryItemConfig { const styles: Styles = { root: { width: '100%', + zIndex: zIndex.aboveOverlay, + position: 'relative', }, headerItemInnerDiv: { paddingLeft: 65, @@ -129,9 +132,6 @@ const styles: Styles = { }; const ETHER_ICON_PATH = '/images/ether.png'; -const ETHER_TOKEN_SYMBOL = 'WETH'; -const ZRX_TOKEN_SYMBOL = 'ZRX'; -const ETHER_SYMBOL = 'ETH'; const ICON_DIMENSION = 24; const TOKEN_AMOUNT_DISPLAY_PRECISION = 3; const BODY_ITEM_KEY = 'BODY'; @@ -322,7 +322,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> { const primaryText = this._renderAmount( this.props.userEtherBalanceInWei, constants.DECIMAL_PLACES_ETH, - ETHER_SYMBOL, + constants.ETHER_SYMBOL, ); const etherToken = this._getEthToken(); const etherPrice = this.state.trackedTokenStateByAddress[etherToken.address].price; @@ -341,13 +341,13 @@ export class Wallet extends React.Component<WalletProps, WalletState> { ? { ...walletItemStyles.focusedItem, ...styles.paddedItem } : { ...styles.tokenItem, ...styles.borderedItem, ...styles.paddedItem }; const key = ETHER_ITEM_KEY; - return this._renderBalanceRow(key, icon, primaryText, secondaryText, accessoryItemConfig); + return this._renderBalanceRow(key, icon, primaryText, secondaryText, accessoryItemConfig, 'eth-row'); } private _renderTokenRows(): React.ReactNode { const trackedTokens = this.props.trackedTokens; const trackedTokensStartingWithEtherToken = trackedTokens.sort( - firstBy((t: Token) => t.symbol !== ETHER_TOKEN_SYMBOL) - .thenBy((t: Token) => t.symbol !== ZRX_TOKEN_SYMBOL) + firstBy((t: Token) => t.symbol !== constants.ETHER_TOKEN_SYMBOL) + .thenBy((t: Token) => t.symbol !== constants.ZRX_TOKEN_SYMBOL) .thenBy('address'), ); return _.map(trackedTokensStartingWithEtherToken, this._renderTokenRow.bind(this)); @@ -362,7 +362,8 @@ export class Wallet extends React.Component<WalletProps, WalletState> { const icon = <TokenIcon token={token} diameter={ICON_DIMENSION} link={tokenLink} />; const primaryText = this._renderAmount(tokenState.balance, token.decimals, token.symbol); const secondaryText = this._renderValue(tokenState.balance, token.decimals, tokenState.price); - const wrappedEtherDirection = token.symbol === ETHER_TOKEN_SYMBOL ? Side.Receive : undefined; + const isWeth = token.symbol === constants.ETHER_TOKEN_SYMBOL; + const wrappedEtherDirection = isWeth ? Side.Receive : undefined; const accessoryItemConfig: AccessoryItemConfig = { wrappedEtherDirection, allowanceToggleConfig: { @@ -371,7 +372,14 @@ export class Wallet extends React.Component<WalletProps, WalletState> { }, }; const key = token.address; - return this._renderBalanceRow(key, icon, primaryText, secondaryText, accessoryItemConfig); + return this._renderBalanceRow( + key, + icon, + primaryText, + secondaryText, + accessoryItemConfig, + isWeth ? 'weth-row' : undefined, + ); } private _renderBalanceRow( key: string, @@ -379,6 +387,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> { primaryText: React.ReactNode, secondaryText: React.ReactNode, accessoryItemConfig: AccessoryItemConfig, + className?: string, ): React.ReactNode { const shouldShowWrapEtherItem = !_.isUndefined(this.state.wrappedEtherDirection) && @@ -388,7 +397,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> { : { ...styles.tokenItem, ...styles.borderedItem, ...styles.paddedItem }; const etherToken = this._getEthToken(); return ( - <div key={key} className="flex flex-column"> + <div key={key} className={`flex flex-column ${className || ''}`}> <div className="flex items-center" style={style}> <div className="px2">{icon}</div> <div className="flex-none pr2 pt2 pb2"> @@ -578,8 +587,6 @@ export class Wallet extends React.Component<WalletProps, WalletState> { }); } private _getEthToken(): Token { - const tokens = _.values(this.props.tokenByAddress); - const etherToken = _.find(tokens, { symbol: ETHER_TOKEN_SYMBOL }); - return etherToken; + return utils.getEthToken(this.props.tokenByAddress); } } // tslint:disable:max-file-line-count diff --git a/packages/website/ts/components/wallet/wallet_disconnected_item.tsx b/packages/website/ts/components/wallet/wallet_disconnected_item.tsx index d334f1748..39a62e1fb 100644 --- a/packages/website/ts/components/wallet/wallet_disconnected_item.tsx +++ b/packages/website/ts/components/wallet/wallet_disconnected_item.tsx @@ -6,6 +6,7 @@ import * as React from 'react'; import { ProviderType } from 'ts/types'; import { colors } from 'ts/utils/colors'; import { constants } from 'ts/utils/constants'; +import { utils } from 'ts/utils/utils'; export interface WalletDisconnectedItemProps { providerType: ProviderType; @@ -38,8 +39,7 @@ const BUTTON_BOTTOM_PADDING = 80; export const WalletDisconnectedItem: React.StatelessComponent<WalletDisconnectedItemProps> = ( props: WalletDisconnectedItemProps, ) => { - const isExternallyInjectedProvider = - props.providerType === ProviderType.Injected && props.injectedProviderName !== '0x Public'; + const isExternallyInjectedProvider = utils.isExternallyInjected(props.providerType, props.injectedProviderName); return ( <div className="flex flex-center"> <div className="mx-auto"> diff --git a/packages/website/ts/containers/portal_onboarding_flow.ts b/packages/website/ts/containers/portal_onboarding_flow.ts index 3cd4e8510..84739192f 100644 --- a/packages/website/ts/containers/portal_onboarding_flow.ts +++ b/packages/website/ts/containers/portal_onboarding_flow.ts @@ -1,7 +1,8 @@ +import { BigNumber } from '@0xproject/utils'; import * as React from 'react'; import { connect } from 'react-redux'; import { Dispatch } from 'redux'; -import { ActionTypes } from 'ts/types'; +import { ActionTypes, ProviderType, TokenByAddress } from 'ts/types'; import { PortalOnboardingFlow as PortalOnboardingFlowComponent } from 'ts/components/onboarding/portal_onboarding_flow'; import { State } from 'ts/redux/reducer'; @@ -11,22 +12,43 @@ interface PortalOnboardingFlowProps {} interface ConnectedState { stepIndex: number; isRunning: boolean; + userAddress: string; + hasBeenSeen: boolean; + providerType: ProviderType; + injectedProviderName: string; + blockchainIsLoaded: boolean; + userEthBalanceInWei: BigNumber; + tokenByAddress: TokenByAddress; } interface ConnectedDispatch { - onClose: () => void; + updateIsRunning: (isRunning: boolean) => void; + updateOnboardingStep: (stepIndex: number) => void; } const mapStateToProps = (state: State): ConnectedState => ({ stepIndex: state.portalOnboardingStep, isRunning: state.isPortalOnboardingShowing, + userAddress: state.userAddress, + providerType: state.providerType, + injectedProviderName: state.injectedProviderName, + blockchainIsLoaded: state.blockchainIsLoaded, + userEthBalanceInWei: state.userEtherBalanceInWei, + tokenByAddress: state.tokenByAddress, + hasBeenSeen: state.hasPortalOnboardingBeenSeen, }); const mapDispatchToProps = (dispatch: Dispatch<State>): ConnectedDispatch => ({ - onClose: (): void => { + updateIsRunning: (isRunning: boolean): void => { dispatch({ type: ActionTypes.UpdatePortalOnboardingShowing, - data: false, + data: isRunning, + }); + }, + updateOnboardingStep: (stepIndex: number): void => { + dispatch({ + type: ActionTypes.UpdatePortalOnboardingStep, + data: stepIndex, }); }, }); diff --git a/packages/website/ts/index.tsx b/packages/website/ts/index.tsx index 4fe81a91e..a6bfe7dd2 100644 --- a/packages/website/ts/index.tsx +++ b/packages/website/ts/index.tsx @@ -1,12 +1,9 @@ -// Polyfills import { MuiThemeProvider } from 'material-ui/styles'; import * as React from 'react'; import { render } from 'react-dom'; import { Provider } from 'react-redux'; import { BrowserRouter as Router, Redirect, Route, Switch } from 'react-router-dom'; import * as injectTapEventPlugin from 'react-tap-event-plugin'; -import { createStore, Store as ReduxStore } from 'redux'; -import { devToolsEnhancer } from 'redux-devtools-extension/developmentOnly'; import { Redirecter } from 'ts/components/redirecter'; import { About } from 'ts/containers/about'; import { FAQ } from 'ts/containers/faq'; @@ -16,11 +13,12 @@ import { Wiki } from 'ts/containers/wiki'; import { createLazyComponent } from 'ts/lazy_component'; import { trackedTokenStorage } from 'ts/local_storage/tracked_token_storage'; import { tradeHistoryStorage } from 'ts/local_storage/trade_history_storage'; -import { reducer, State } from 'ts/redux/reducer'; +import { store } from 'ts/redux/store'; import { WebsiteLegacyPaths, WebsitePaths } from 'ts/types'; import { analytics } from 'ts/utils/analytics'; import { muiTheme } from 'ts/utils/mui_theme'; import { utils } from 'ts/utils/utils'; +// Polyfills import 'whatwg-fetch'; injectTapEventPlugin(); @@ -75,7 +73,7 @@ const LazyOrderUtilsDocumentation = createLazyComponent('Documentation', async ( analytics.init(); // tslint:disable-next-line:no-floating-promises analytics.logProviderAsync((window as any).web3); -const store: ReduxStore<State> = createStore(reducer, devToolsEnhancer({ name: '0x Website Redux Store' })); + render( <Router> <div> diff --git a/packages/website/ts/local_storage/local_storage.ts b/packages/website/ts/local_storage/local_storage.ts index 20a533a91..1e3258ce0 100644 --- a/packages/website/ts/local_storage/local_storage.ts +++ b/packages/website/ts/local_storage/local_storage.ts @@ -26,6 +26,16 @@ export const localStorage = { } window.localStorage.removeItem(key); }, + getObject(key: string): object | undefined { + const item = localStorage.getItemIfExists(key); + if (item) { + return JSON.parse(item); + } + return undefined; + }, + setObject(key: string, value: object): void { + localStorage.setItem(key, JSON.stringify(value)); + }, getAllKeys(): string[] { if (!this.doesExist) { return []; diff --git a/packages/website/ts/local_storage/state_storage.ts b/packages/website/ts/local_storage/state_storage.ts new file mode 100644 index 000000000..517784b5b --- /dev/null +++ b/packages/website/ts/local_storage/state_storage.ts @@ -0,0 +1,16 @@ +import { localStorage } from 'ts/local_storage/local_storage'; +import { INITIAL_STATE, State } from 'ts/redux/reducer'; + +const STORAGE_NAME = 'persistedState'; + +export const stateStorage = { + saveState(partialState: Partial<State>): void { + localStorage.setObject(STORAGE_NAME, partialState); + }, + getPersistedState(): Partial<State> { + return localStorage.getObject(STORAGE_NAME); + }, + getPersistedDefaultState(): State { + return { ...INITIAL_STATE, ...stateStorage.getPersistedState() }; + }, +}; diff --git a/packages/website/ts/redux/reducer.ts b/packages/website/ts/redux/reducer.ts index e61345c87..5c57792f7 100644 --- a/packages/website/ts/redux/reducer.ts +++ b/packages/website/ts/redux/reducer.ts @@ -42,6 +42,7 @@ export interface State { userEtherBalanceInWei: BigNumber; portalOnboardingStep: number; isPortalOnboardingShowing: boolean; + hasPortalOnboardingBeenSeen: boolean; // Note: cache of supplied orderJSON in fill order step. Do not use for anything else. userSuppliedOrderCache: Order; @@ -56,7 +57,7 @@ export interface State { translate: Translate; } -const INITIAL_STATE: State = { +export const INITIAL_STATE: State = { // Portal blockchainErr: BlockchainErrs.NoError, blockchainIsLoaded: false, @@ -84,6 +85,7 @@ const INITIAL_STATE: State = { userSuppliedOrderCache: undefined, portalOnboardingStep: 0, isPortalOnboardingShowing: false, + hasPortalOnboardingBeenSeen: false, // Docs docsVersion: DEFAULT_DOCS_VERSION, availableDocVersions: [DEFAULT_DOCS_VERSION], @@ -309,6 +311,7 @@ export function reducer(state: State = INITIAL_STATE, action: Action): State { return { ...state, isPortalOnboardingShowing, + hasPortalOnboardingBeenSeen: true, }; } diff --git a/packages/website/ts/redux/store.ts b/packages/website/ts/redux/store.ts new file mode 100644 index 000000000..203f068a1 --- /dev/null +++ b/packages/website/ts/redux/store.ts @@ -0,0 +1,21 @@ +import * as _ from 'lodash'; +import { createStore, Store as ReduxStore } from 'redux'; +import { devToolsEnhancer } from 'redux-devtools-extension/developmentOnly'; +import { stateStorage } from 'ts/local_storage/state_storage'; +import { reducer, State } from 'ts/redux/reducer'; + +const ONE_SECOND = 1000; + +export const store: ReduxStore<State> = createStore( + reducer, + stateStorage.getPersistedDefaultState(), + devToolsEnhancer({ name: '0x Website Redux Store' }), +); +store.subscribe( + _.throttle(() => { + // Persisted state + stateStorage.saveState({ + hasPortalOnboardingBeenSeen: store.getState().hasPortalOnboardingBeenSeen, + }); + }, ONE_SECOND), +); diff --git a/packages/website/ts/utils/constants.ts b/packages/website/ts/utils/constants.ts index f8710b349..9dc1d492c 100644 --- a/packages/website/ts/utils/constants.ts +++ b/packages/website/ts/utils/constants.ts @@ -4,6 +4,9 @@ import { BigNumber } from '@0xproject/utils'; export const constants = { DECIMAL_PLACES_ETH: 18, DECIMAL_PLACES_ZRX: 18, + ETHER_TOKEN_SYMBOL: 'WETH', + ZRX_TOKEN_SYMBOL: 'ZRX', + ETHER_SYMBOL: 'ETH', GENESIS_ORDER_BLOCK_BY_NETWORK_ID: { 1: 4145578, 42: 3117574, diff --git a/packages/website/ts/utils/style.ts b/packages/website/ts/utils/style.ts index 51b6e4eb6..0411cdd91 100644 --- a/packages/website/ts/utils/style.ts +++ b/packages/website/ts/utils/style.ts @@ -1,4 +1,5 @@ export const zIndex = { topBar: 1100, - overlay: 1101, + overlay: 1105, + aboveOverlay: 1106, }; diff --git a/packages/website/ts/utils/utils.ts b/packages/website/ts/utils/utils.ts index e79a873e0..b9d962b75 100644 --- a/packages/website/ts/utils/utils.ts +++ b/packages/website/ts/utils/utils.ts @@ -11,6 +11,7 @@ import { Environments, Order, Providers, + ProviderType, ScreenWidths, Side, SideToAssetToken, @@ -313,8 +314,16 @@ export const utils = { isStaging(): boolean { return _.includes(window.location.href, configs.DOMAIN_STAGING); }, + isExternallyInjected(providerType: ProviderType, injectedProviderName: string): boolean { + return providerType === ProviderType.Injected && injectedProviderName !== constants.PROVIDER_NAME_PUBLIC; + }, isDogfood, shouldShowPortalV2(): boolean { return this.isDevelopment() || this.isStaging() || this.isDogfood(); }, + getEthToken(tokenByAddress: TokenByAddress): Token { + const tokens = _.values(tokenByAddress); + const etherToken = _.find(tokens, { symbol: constants.ETHER_TOKEN_SYMBOL }); + return etherToken; + }, }; |