diff options
author | Fabio Berger <me@fabioberger.com> | 2018-06-01 01:45:34 +0800 |
---|---|---|
committer | Fabio Berger <me@fabioberger.com> | 2018-06-01 01:45:34 +0800 |
commit | 94ee82e076d85b64063b5c71be13b1ebe0bb8c10 (patch) | |
tree | 53524249da4c5b907e1489d8677a7cc1edf069a2 /packages/website/ts | |
parent | 0beab9eec45508fb6163bd6c0fd3970f0b61a91d (diff) | |
parent | 5b31d0aa3635ea524fb42d73cd6c713887dfef6a (diff) | |
download | dexon-sol-tools-94ee82e076d85b64063b5c71be13b1ebe0bb8c10.tar dexon-sol-tools-94ee82e076d85b64063b5c71be13b1ebe0bb8c10.tar.gz dexon-sol-tools-94ee82e076d85b64063b5c71be13b1ebe0bb8c10.tar.bz2 dexon-sol-tools-94ee82e076d85b64063b5c71be13b1ebe0bb8c10.tar.lz dexon-sol-tools-94ee82e076d85b64063b5c71be13b1ebe0bb8c10.tar.xz dexon-sol-tools-94ee82e076d85b64063b5c71be13b1ebe0bb8c10.tar.zst dexon-sol-tools-94ee82e076d85b64063b5c71be13b1ebe0bb8c10.zip |
Merge branch 'v2-prototype' into refactor/order-utils/for-v2
* v2-prototype: (45 commits)
Check length before accessing indices, add awaitTransactionSuccess where needed, and rename function
Add back before/after snapshots for each test
Rename Signer to Wallet, rename GAS_ESTIMATE to GAS_LIMIT
Make preSigned and allowedValidators mappings public
Change names of signature types
Fix formatting and tests
Make AssetProxyId last byte of assetData
Add signer to txHash, allow approveValidator to be used with executeTransaction
Update Whitelist
Fix Exchange interface
Increase block gas limit
Use last byte of signature as signature type
Remove TxOrigin signature type, modify whitelist to use Validator signature type
Update Whitelist contract with comments, also require maker to be whitelisted
Fix build
Add example whitelist contract and minimum tests
Add sample whitelist contract
Add TxOrigin signature type and rearrange order of types
Add approveValidator function
Add Validator signature type
...
# Conflicts:
# packages/contracts/src/contracts/current/protocol/Exchange/MixinSignatureValidator.sol
# packages/contracts/src/utils/types.ts
# packages/contracts/test/exchange/transactions.ts
# packages/order-utils/src/asset_proxy_utils.ts
Diffstat (limited to 'packages/website/ts')
18 files changed, 405 insertions, 69 deletions
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/portal/drawer_menu.tsx b/packages/website/ts/components/portal/drawer_menu.tsx index 75c8ac6c2..ace11639a 100644 --- a/packages/website/ts/components/portal/drawer_menu.tsx +++ b/packages/website/ts/components/portal/drawer_menu.tsx @@ -36,7 +36,7 @@ export interface DrawerMenuProps { } export const DrawerMenu = (props: DrawerMenuProps) => { const relayerItemEntry = { - to: `${WebsitePaths.Portal}/`, + to: `${WebsitePaths.Portal}`, labelText: 'Relayer ecosystem', iconName: 'zmdi-portable-wifi', }; 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; + }, }; |