Merge pull request #635 from 0xProject/feature/website/custom-onboarding-tooltip

Remove react-joyride and some more refactoring
This commit is contained in:
Francesco Agosti 2018-05-30 11:49:04 -07:00 committed by GitHub
commit e18d61b31a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 530 additions and 277 deletions

View File

@ -1,4 +1,12 @@
[
{
"version": "0.5.0",
"changes": [
{
"note": "Add types for `react-popper`, remove types for `react-joyride`"
}
]
},
{
"version": "0.4.0",
"changes": [

View File

@ -1,86 +0,0 @@
// Type definitions for react-joyride 2.0.0-11
// Project: https://github.com/gilbarbara/react-joyride
declare module 'react-joyride' {
import * as React from 'react';
export interface StyleOptions {
arrowColor?: string;
backgroundColor?: string;
primaryColor?: string;
textColor?: string;
overlayColor?: string;
spotlightShadow?: string;
beaconSize?: number;
zIndex?: number;
}
export type Placement =
| 'top'
| 'top-left'
| 'top-right'
| 'bottom'
| 'bottom-left'
| 'bottom-right'
| 'right'
| 'left';
export interface Step {
title?: string;
content: React.ReactNode;
target: string;
placement?: Placement;
type?: 'click' | 'hover';
isFixed?: boolean;
allowClicksThruHole?: boolean;
disableBeacon?: boolean;
style?: StyleOptions;
[prop: string]: any;
}
export interface StyleOptionsProp {
options: StyleOptions;
}
interface CallbackMetadata {
type:
| 'tour:start'
| 'step:before'
| 'beacon'
| 'tooltip'
| 'close'
| 'step:after'
| 'tour:end'
| 'tour:status'
| 'error:target_not_found'
| 'error';
step: number;
}
export type CallbackData = CallbackMetadata & State;
export interface Props {
steps?: Step[];
beaconComponent?: React.ReactNode;
disableOverlayClose?: boolean;
run?: boolean;
stepIndex?: number;
callback?: (data: CallbackData) => void;
debug?: boolean;
styles?: StyleOptionsProp;
}
export interface State {
action: string;
controlled: boolean;
index: number;
lifecycle: string;
size: 0;
status: string;
}
export default class Joyride extends React.Component<Props, State> {
constructor(props: Props);
static defaultProps: Props;
}
}

View File

@ -0,0 +1,49 @@
// Type definitions for react-popper 1.0.0-beta.6
// Project: https://github.com/gilbarbara/react-joyride
declare module 'react-popper' {
import * as React from 'react';
import * as PopperJS from 'popper.js';
interface ManagerProps {
children: React.ReactNode;
}
export class Manager extends React.Component<ManagerProps, {}> {}
type RefHandler = (ref: HTMLElement | null) => void;
export interface ReferenceChildrenProps {
ref: RefHandler;
}
export interface ReferenceProps {
children: (props: ReferenceChildrenProps) => React.ReactNode;
}
export class Reference extends React.Component<ReferenceProps, {}> {}
export interface PopperArrowProps {
ref: RefHandler;
style: React.CSSProperties;
}
export type Placement = PopperJS.Placement;
export interface PopperChildrenProps {
arrowProps: PopperArrowProps;
outOfBoundaries: boolean | null;
placement: PopperJS.Placement;
ref: RefHandler;
scheduleUpdate: () => void;
style: React.CSSProperties;
}
export interface PopperProps {
children: (props: PopperChildrenProps) => React.ReactNode;
eventsEnabled?: boolean;
modifiers?: PopperJS.Modifiers;
placement?: PopperJS.Placement;
positionFixed?: boolean;
referenceElement?: Element;
}
export class Popper extends React.Component<PopperProps, {}> {}
}

View File

@ -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",

View File

@ -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>

View File

@ -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 (
<Joyride
run={this.props.isRunning}
debug={true}
steps={this.props.steps}
stepIndex={this.props.stepIndex}
styles={{ options: joyrideStyleOptions }}
callback={this._handleChange.bind(this)}
/>
<Overlay>
<Popper referenceElement={this._getElementForStep()} placement={this._getCurrentStep().placement}>
{this._renderPopperChildren.bind(this)}
</Popper>
</Overlay>
);
}
private _handleChange(data: CallbackData): void {
switch (data.action) {
case 'close':
this.props.onClose();
private _getElementForStep(): Element {
return document.querySelector(this._getCurrentStep().target);
}
private _renderPopperChildren(props: PopperChildrenProps): React.ReactNode {
return (
<div ref={props.ref} style={props.style} data-placement={props.placement}>
{this._renderToolTip()}
</div>
);
}
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();
}
}
}

View File

@ -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';

View File

@ -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);
}
}
}

View File

@ -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}

View File

@ -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;
}

View File

@ -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';

View File

@ -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

View File

@ -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">

View File

@ -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,
});
},
});

View File

@ -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>

View File

@ -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 [];

View File

@ -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() };
},
};

View File

@ -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,
};
}

View File

@ -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),
);

View File

@ -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,

View File

@ -1,4 +1,5 @@
export const zIndex = {
topBar: 1100,
overlay: 1101,
overlay: 1105,
aboveOverlay: 1106,
};

View File

@ -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;
},
};

109
yarn.lock
View File

@ -1443,7 +1443,7 @@ babel-register@^6.26.0, babel-register@^6.9.0:
mkdirp "^0.5.1"
source-map-support "^0.4.15"
babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtime@^6.20.0, babel-runtime@^6.22.0, babel-runtime@^6.23.0, babel-runtime@^6.26.0:
babel-runtime@6.x.x, babel-runtime@^6.11.6, babel-runtime@^6.18.0, babel-runtime@^6.20.0, babel-runtime@^6.22.0, babel-runtime@^6.23.0, babel-runtime@^6.26.0:
version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe"
dependencies:
@ -2890,6 +2890,13 @@ create-react-class@^15.5.2, create-react-class@^15.6.0:
loose-envify "^1.3.1"
object-assign "^4.1.1"
create-react-context@^0.2.1:
version "0.2.2"
resolved "https://registry.npmjs.org/create-react-context/-/create-react-context-0.2.2.tgz#9836542f9aaa22868cd7d4a6f82667df38019dca"
dependencies:
fbjs "^0.8.0"
gud "^1.0.0"
cross-fetch@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-2.1.0.tgz#7d4ea7e10a4f3bb73d5c2f0a3791ec3752d39b50"
@ -3246,14 +3253,6 @@ dedent@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c"
deep-diff@^0.3.8:
version "0.3.8"
resolved "https://registry.yarnpkg.com/deep-diff/-/deep-diff-0.3.8.tgz#c01de63efb0eec9798801d40c7e0dae25b582c84"
deep-diff@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/deep-diff/-/deep-diff-1.0.0.tgz#0dd55f9412f22a07b2edbfbb11bb4633be6be40b"
deep-eql@^0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-0.1.3.tgz#ef558acab8de25206cd713906d74e56930eb69f2"
@ -3286,10 +3285,6 @@ deep-is@~0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
deepmerge@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.1.0.tgz#511a54fff405fc346f0240bb270a3e9533a31102"
default-require-extensions@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-1.0.0.tgz#f37ea15d3e13ffd9b437d33e1a75b5fb97874cb8"
@ -4203,7 +4198,7 @@ execa@^0.8.0:
signal-exit "^3.0.0"
strip-eof "^1.0.0"
exenv@^1.2.1, exenv@^1.2.2:
exenv@^1.2.1:
version "1.2.2"
resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d"
@ -4395,7 +4390,7 @@ faye-websocket@~0.11.0:
dependencies:
websocket-driver ">=0.5.1"
fbjs@^0.8.1, fbjs@^0.8.16, fbjs@^0.8.4, fbjs@^0.8.6, fbjs@^0.8.9:
fbjs@^0.8.0, fbjs@^0.8.1, fbjs@^0.8.16, fbjs@^0.8.4, fbjs@^0.8.6, fbjs@^0.8.9:
version "0.8.16"
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db"
dependencies:
@ -5219,6 +5214,10 @@ growl@1.9.2:
version "1.9.2"
resolved "https://registry.yarnpkg.com/growl/-/growl-1.9.2.tgz#0ea7743715db8d8de2c5ede1775e1b45ac85c02f"
gud@^1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0"
gulp-sourcemaps@1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/gulp-sourcemaps/-/gulp-sourcemaps-1.6.0.tgz#b86ff349d801ceb56e1d9e7dc7bbcb4b7dee600c"
@ -6041,10 +6040,6 @@ is-installed-globally@^0.1.0:
global-dirs "^0.1.0"
is-path-inside "^1.0.0"
is-lite@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/is-lite/-/is-lite-0.2.0.tgz#1532415467f1fd49aa693ba013a8669575df7cf3"
is-mobile@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/is-mobile/-/is-mobile-0.2.2.tgz#0e2e006d99ed2c2155b761df80f2a3619ae2ad9f"
@ -7760,10 +7755,6 @@ neo-async@^2.5.0:
version "2.5.1"
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.5.1.tgz#acb909e327b1e87ec9ef15f41b8a269512ad41ee"
nested-property@0.0.7, nested-property@^0.0.7:
version "0.0.7"
resolved "https://registry.yarnpkg.com/nested-property/-/nested-property-0.0.7.tgz#ff222f233ca8793c6828b4117091bea597130f4f"
newman@^3.9.3:
version "3.9.3"
resolved "https://registry.yarnpkg.com/newman/-/newman-3.9.3.tgz#939356026942474ba15482bd37a15c60bb200ca0"
@ -8612,7 +8603,7 @@ plur@^2.1.2:
dependencies:
irregular-plurals "^1.0.0"
popper.js@^1.14.3:
popper.js@^1.14.1:
version "1.14.3"
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.3.tgz#1438f98d046acf7b4d78cd502bf418ac64d4f095"
@ -9318,12 +9309,6 @@ quick-lru@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8"
rafl@~1.2.1:
version "1.2.2"
resolved "https://registry.yarnpkg.com/rafl/-/rafl-1.2.2.tgz#fe930f758211020d47e38815f5196a8be4150740"
dependencies:
global "~4.3.0"
randomatic@^1.1.3:
version "1.1.7"
resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-1.1.7.tgz#c7abe9cc8b87c0baa876b19fde83fd464797e38c"
@ -9430,16 +9415,6 @@ react-event-listener@^0.4.5:
prop-types "^15.5.4"
warning "^3.0.0"
react-floater@^0.5.4:
version "0.5.4"
resolved "https://registry.yarnpkg.com/react-floater/-/react-floater-0.5.4.tgz#411a57fd8631e96466e035ee6c91f1f118c8782a"
dependencies:
deepmerge "^2.1.0"
exenv "^1.2.2"
is-lite "^0.2.0"
popper.js "^1.14.3"
react-proptype-conditional-require "^1.0.4"
react-ga@^2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/react-ga/-/react-ga-2.4.1.tgz#dfbd5f028ed39a07067f7a8bf57dc0d240000767"
@ -9456,22 +9431,6 @@ react-highlight@0xproject/react-highlight:
react "^15.5.4"
react-dom "^15.5.4"
react-joyride@^2.0.0-11:
version "2.0.0-11"
resolved "https://registry.yarnpkg.com/react-joyride/-/react-joyride-2.0.0-11.tgz#5cbf4f86b83dfbf9242e7d19482a0fd7cc69ad52"
dependencies:
deep-diff "^1.0.0"
deepmerge "^2.1.0"
exenv "^1.2.2"
is-lite "^0.2.0"
nested-property "^0.0.7"
react-floater "^0.5.4"
react-proptype-conditional-require "^1.0.4"
scroll "^2.0.3"
scroll-doc "^0.2.1"
scrollparent "^2.0.1"
tree-changes "^0.3.2"
react-markdown@^3.2.2:
version "3.3.0"
resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-3.3.0.tgz#a87cdd822aa9302d6add9687961dd1a82a45d02e"
@ -9482,9 +9441,16 @@ react-markdown@^3.2.2:
unist-util-visit "^1.3.0"
xtend "^4.0.1"
react-proptype-conditional-require@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/react-proptype-conditional-require/-/react-proptype-conditional-require-1.0.4.tgz#69c2d5741e6df5e08f230f36bbc2944ee1222555"
react-popper@^1.0.0-beta.6:
version "1.0.0-beta.6"
resolved "https://registry.npmjs.org/react-popper/-/react-popper-1.0.0-beta.6.tgz#cb27a2ac56adccbaf5f9c4132387289069240834"
dependencies:
babel-runtime "6.x.x"
create-react-context "^0.2.1"
popper.js "^1.14.1"
prop-types "^15.6.1"
typed-styles "^0.0.5"
warning "^3.0.0"
react-redux@^5.0.3:
version "5.0.7"
@ -10175,20 +10141,6 @@ scoped-regex@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/scoped-regex/-/scoped-regex-1.0.0.tgz#a346bb1acd4207ae70bd7c0c7ca9e566b6baddb8"
scroll-doc@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/scroll-doc/-/scroll-doc-0.2.1.tgz#168f9e9ac598743dd682c992839b44a38ce4dd91"
scroll@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/scroll/-/scroll-2.0.3.tgz#0951b785544205fd17753bc3d294738ba16fc2ab"
dependencies:
rafl "~1.2.1"
scrollparent@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/scrollparent/-/scrollparent-2.0.1.tgz#715d5b9cc57760fb22bdccc3befb5bfe06b1a317"
scrypt-js@2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/scrypt-js/-/scrypt-js-2.0.3.tgz#bb0040be03043da9a012a2cea9fc9f852cfc87d4"
@ -11491,13 +11443,6 @@ traverse-chain@~0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/traverse-chain/-/traverse-chain-0.1.0.tgz#61dbc2d53b69ff6091a12a168fd7d433107e40f1"
tree-changes@^0.3.2:
version "0.3.2"
resolved "https://registry.yarnpkg.com/tree-changes/-/tree-changes-0.3.2.tgz#ad7d3b499155bd6176f2c9c6a472d8155df0a9ce"
dependencies:
deep-diff "^0.3.8"
nested-property "0.0.7"
treeify@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/treeify/-/treeify-1.1.0.tgz#4e31c6a463accd0943879f30667c4fdaff411bb8"
@ -11663,6 +11608,10 @@ type-is@~1.6.15, type-is@~1.6.16:
media-typer "0.3.0"
mime-types "~2.1.18"
typed-styles@^0.0.5:
version "0.0.5"
resolved "https://registry.npmjs.org/typed-styles/-/typed-styles-0.0.5.tgz#a60df245d482a9b1adf9c06c078d0f06085ed1cf"
typedarray-to-buffer@^3.1.2:
version "3.1.5"
resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"