Merge pull request #910 from 0xProject/feature/website/upgrade-allowance-toggles-to-locks-and-checks
[website] Use new designs with tooltips for allowance toggles
This commit is contained in:
160
packages/website/ts/components/inputs/allowance_state_toggle.tsx
Normal file
160
packages/website/ts/components/inputs/allowance_state_toggle.tsx
Normal file
@@ -0,0 +1,160 @@
|
||||
import { colors } from '@0xproject/react-shared';
|
||||
import { BigNumber, logUtils } from '@0xproject/utils';
|
||||
import * as _ from 'lodash';
|
||||
import * as React from 'react';
|
||||
import ReactTooltip = require('react-tooltip');
|
||||
import { Blockchain } from 'ts/blockchain';
|
||||
import { AllowanceState, AllowanceStateView } from 'ts/components/ui/allowance_state_view';
|
||||
import { Container } from 'ts/components/ui/container';
|
||||
import { PointerDirection } from 'ts/components/ui/pointer';
|
||||
import { Text } from 'ts/components/ui/text';
|
||||
import { Dispatcher } from 'ts/redux/dispatcher';
|
||||
import { BalanceErrs, Token, TokenState } from 'ts/types';
|
||||
import { analytics } from 'ts/utils/analytics';
|
||||
import { errorReporter } from 'ts/utils/error_reporter';
|
||||
import { utils } from 'ts/utils/utils';
|
||||
|
||||
export interface AllowanceStateToggleProps {
|
||||
networkId: number;
|
||||
blockchain: Blockchain;
|
||||
dispatcher: Dispatcher;
|
||||
token: Token;
|
||||
tokenState: TokenState;
|
||||
userAddress: string;
|
||||
onErrorOccurred?: (errType: BalanceErrs) => void;
|
||||
refetchTokenStateAsync: () => Promise<void>;
|
||||
tooltipDirection?: PointerDirection;
|
||||
}
|
||||
|
||||
export interface AllowanceStateToggleState {
|
||||
allowanceState: AllowanceState;
|
||||
prevTokenState: TokenState;
|
||||
loadingMessage?: string;
|
||||
}
|
||||
|
||||
const DEFAULT_ALLOWANCE_AMOUNT_IN_BASE_UNITS = new BigNumber(2).pow(256).minus(1);
|
||||
|
||||
export class AllowanceStateToggle extends React.Component<AllowanceStateToggleProps, AllowanceStateToggleState> {
|
||||
public static defaultProps = {
|
||||
onErrorOccurred: _.noop.bind(_),
|
||||
tooltipDirection: PointerDirection.Right,
|
||||
};
|
||||
private static _getAllowanceState(tokenState: TokenState): AllowanceState {
|
||||
if (!tokenState.isLoaded) {
|
||||
return AllowanceState.Loading;
|
||||
}
|
||||
if (tokenState.allowance.gt(0)) {
|
||||
return AllowanceState.Unlocked;
|
||||
}
|
||||
return AllowanceState.Locked;
|
||||
}
|
||||
constructor(props: AllowanceStateToggleProps) {
|
||||
super(props);
|
||||
const tokenState = props.tokenState;
|
||||
this.state = {
|
||||
allowanceState: AllowanceStateToggle._getAllowanceState(tokenState),
|
||||
prevTokenState: tokenState,
|
||||
};
|
||||
}
|
||||
|
||||
public render(): React.ReactNode {
|
||||
const tooltipId = `tooltip-id-${this.props.token.symbol}`;
|
||||
return (
|
||||
<Container cursor="pointer">
|
||||
<ReactTooltip id={tooltipId} effect="solid" offset={{ top: 3 }}>
|
||||
{this._getTooltipContent()}
|
||||
</ReactTooltip>
|
||||
<div
|
||||
data-tip={true}
|
||||
data-for={tooltipId}
|
||||
data-place={this.props.tooltipDirection}
|
||||
onClick={this._onToggleAllowanceAsync.bind(this)}
|
||||
>
|
||||
<AllowanceStateView allowanceState={this.state.allowanceState} />
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
public componentWillReceiveProps(nextProps: AllowanceStateToggleProps): void {
|
||||
const nextTokenState = nextProps.tokenState;
|
||||
const prevTokenState = this.state.prevTokenState;
|
||||
if (
|
||||
!nextTokenState.allowance.eq(prevTokenState.allowance) ||
|
||||
nextTokenState.isLoaded !== prevTokenState.isLoaded
|
||||
) {
|
||||
const tokenState = nextProps.tokenState;
|
||||
this.setState({
|
||||
prevTokenState: tokenState,
|
||||
allowanceState: AllowanceStateToggle._getAllowanceState(nextTokenState),
|
||||
});
|
||||
}
|
||||
}
|
||||
private _getTooltipContent(): React.ReactNode {
|
||||
const symbol = this.props.token.symbol;
|
||||
switch (this.state.allowanceState) {
|
||||
case AllowanceState.Loading:
|
||||
return (
|
||||
<Text noWrap={true} fontColor={colors.white}>
|
||||
{this.state.loadingMessage || 'Loading...'}
|
||||
</Text>
|
||||
);
|
||||
case AllowanceState.Locked:
|
||||
return (
|
||||
<Text noWrap={true} fontColor={colors.white}>
|
||||
Click to enable <b>{symbol}</b> for trading
|
||||
</Text>
|
||||
);
|
||||
case AllowanceState.Unlocked:
|
||||
return (
|
||||
<Text noWrap={true} fontColor={colors.white}>
|
||||
<b>{symbol}</b> is available for trading
|
||||
</Text>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
private async _onToggleAllowanceAsync(): Promise<void> {
|
||||
// Close all tooltips
|
||||
ReactTooltip.hide();
|
||||
if (this.props.userAddress === '') {
|
||||
this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
let newAllowanceAmountInBaseUnits = new BigNumber(0);
|
||||
if (!this._isAllowanceSet()) {
|
||||
newAllowanceAmountInBaseUnits = DEFAULT_ALLOWANCE_AMOUNT_IN_BASE_UNITS;
|
||||
}
|
||||
const isUnlockingToken = newAllowanceAmountInBaseUnits.gt(0);
|
||||
this.setState({
|
||||
allowanceState: AllowanceState.Loading,
|
||||
loadingMessage: `${isUnlockingToken ? 'Unlocking' : 'Locking'} ${this.props.token.symbol}`,
|
||||
});
|
||||
const logData = {
|
||||
tokenSymbol: this.props.token.symbol,
|
||||
newAllowance: newAllowanceAmountInBaseUnits.toNumber(),
|
||||
};
|
||||
try {
|
||||
await this.props.blockchain.setProxyAllowanceAsync(this.props.token, newAllowanceAmountInBaseUnits);
|
||||
analytics.track('Set Allowances Success', logData);
|
||||
await this.props.refetchTokenStateAsync();
|
||||
} catch (err) {
|
||||
analytics.track('Set Allowance Failure', logData);
|
||||
this.setState({
|
||||
allowanceState: AllowanceStateToggle._getAllowanceState(this.state.prevTokenState),
|
||||
});
|
||||
const errMsg = `${err}`;
|
||||
if (utils.didUserDenyWeb3Request(errMsg)) {
|
||||
return;
|
||||
}
|
||||
logUtils.log(`Unexpected error encountered: ${err}`);
|
||||
logUtils.log(err.stack);
|
||||
this.props.onErrorOccurred(BalanceErrs.allowanceSettingFailed);
|
||||
errorReporter.report(err);
|
||||
}
|
||||
}
|
||||
private _isAllowanceSet(): boolean {
|
||||
return !this.props.tokenState.allowance.eq(0);
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
import { Styles } from '@0xproject/react-shared';
|
||||
import { BigNumber, logUtils } from '@0xproject/utils';
|
||||
import * as _ from 'lodash';
|
||||
import Toggle from 'material-ui/Toggle';
|
||||
import * as React from 'react';
|
||||
import { Blockchain } from 'ts/blockchain';
|
||||
import { Dispatcher } from 'ts/redux/dispatcher';
|
||||
import { colors } from 'ts/style/colors';
|
||||
import { BalanceErrs, Token, TokenState } from 'ts/types';
|
||||
import { analytics } from 'ts/utils/analytics';
|
||||
import { errorReporter } from 'ts/utils/error_reporter';
|
||||
import { utils } from 'ts/utils/utils';
|
||||
|
||||
const DEFAULT_ALLOWANCE_AMOUNT_IN_BASE_UNITS = new BigNumber(2).pow(256).minus(1);
|
||||
|
||||
interface AllowanceToggleProps {
|
||||
networkId: number;
|
||||
blockchain: Blockchain;
|
||||
dispatcher: Dispatcher;
|
||||
token: Token;
|
||||
tokenState: TokenState;
|
||||
userAddress: string;
|
||||
isDisabled?: boolean;
|
||||
onErrorOccurred?: (errType: BalanceErrs) => void;
|
||||
refetchTokenStateAsync: () => Promise<void>;
|
||||
}
|
||||
|
||||
interface AllowanceToggleState {
|
||||
isSpinnerVisible: boolean;
|
||||
prevAllowance: BigNumber;
|
||||
}
|
||||
|
||||
const styles: Styles = {
|
||||
baseThumbStyle: {
|
||||
height: 10,
|
||||
width: 10,
|
||||
top: 6,
|
||||
backgroundColor: colors.white,
|
||||
boxShadow: `0px 0px 0px ${colors.allowanceToggleShadow}`,
|
||||
},
|
||||
offThumbStyle: {
|
||||
left: 4,
|
||||
},
|
||||
onThumbStyle: {
|
||||
left: 25,
|
||||
},
|
||||
baseTrackStyle: {
|
||||
width: 25,
|
||||
},
|
||||
offTrackStyle: {
|
||||
backgroundColor: colors.grey300,
|
||||
},
|
||||
onTrackStyle: {
|
||||
backgroundColor: colors.mediumBlue,
|
||||
},
|
||||
};
|
||||
|
||||
export class AllowanceToggle extends React.Component<AllowanceToggleProps, AllowanceToggleState> {
|
||||
public static defaultProps = {
|
||||
onErrorOccurred: _.noop.bind(_),
|
||||
isDisabled: false,
|
||||
};
|
||||
constructor(props: AllowanceToggleProps) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isSpinnerVisible: false,
|
||||
prevAllowance: props.tokenState.allowance,
|
||||
};
|
||||
}
|
||||
public componentWillReceiveProps(nextProps: AllowanceToggleProps): void {
|
||||
if (!nextProps.tokenState.allowance.eq(this.state.prevAllowance)) {
|
||||
this.setState({
|
||||
isSpinnerVisible: false,
|
||||
prevAllowance: nextProps.tokenState.allowance,
|
||||
});
|
||||
}
|
||||
}
|
||||
public render(): React.ReactNode {
|
||||
return (
|
||||
<div className="flex">
|
||||
<div>
|
||||
<Toggle
|
||||
disabled={this.state.isSpinnerVisible || this.props.isDisabled}
|
||||
toggled={this._isAllowanceSet()}
|
||||
onToggle={this._onToggleAllowanceAsync.bind(this)}
|
||||
thumbStyle={{ ...styles.baseThumbStyle, ...styles.offThumbStyle }}
|
||||
thumbSwitchedStyle={{ ...styles.baseThumbStyle, ...styles.onThumbStyle }}
|
||||
trackStyle={{ ...styles.baseTrackStyle, ...styles.offTrackStyle }}
|
||||
trackSwitchedStyle={{ ...styles.baseTrackStyle, ...styles.onTrackStyle }}
|
||||
/>
|
||||
</div>
|
||||
{this.state.isSpinnerVisible && (
|
||||
<div className="pl1" style={{ paddingTop: 3 }}>
|
||||
<i className="zmdi zmdi-spinner zmdi-hc-spin" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private async _onToggleAllowanceAsync(): Promise<void> {
|
||||
if (this.props.userAddress === '') {
|
||||
this.props.dispatcher.updateShouldBlockchainErrDialogBeOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
isSpinnerVisible: true,
|
||||
});
|
||||
|
||||
let newAllowanceAmountInBaseUnits = new BigNumber(0);
|
||||
if (!this._isAllowanceSet()) {
|
||||
newAllowanceAmountInBaseUnits = DEFAULT_ALLOWANCE_AMOUNT_IN_BASE_UNITS;
|
||||
}
|
||||
const logData = {
|
||||
tokenSymbol: this.props.token.symbol,
|
||||
newAllowance: newAllowanceAmountInBaseUnits.toNumber(),
|
||||
};
|
||||
try {
|
||||
await this.props.blockchain.setProxyAllowanceAsync(this.props.token, newAllowanceAmountInBaseUnits);
|
||||
analytics.track('Set Allowances Success', logData);
|
||||
await this.props.refetchTokenStateAsync();
|
||||
} catch (err) {
|
||||
analytics.track('Set Allowance Failure', logData);
|
||||
this.setState({
|
||||
isSpinnerVisible: false,
|
||||
});
|
||||
const errMsg = `${err}`;
|
||||
if (utils.didUserDenyWeb3Request(errMsg)) {
|
||||
return;
|
||||
}
|
||||
logUtils.log(`Unexpected error encountered: ${err}`);
|
||||
logUtils.log(err.stack);
|
||||
this.props.onErrorOccurred(BalanceErrs.allowanceSettingFailed);
|
||||
errorReporter.report(err);
|
||||
}
|
||||
}
|
||||
private _isAllowanceSet(): boolean {
|
||||
return !this.props.tokenState.allowance.eq(0);
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export const OnboardingTooltip: React.StatelessComponent<OnboardingTooltipProps>
|
||||
);
|
||||
};
|
||||
OnboardingTooltip.defaultProps = {
|
||||
pointerDisplay: 'left',
|
||||
pointerDisplay: PointerDirection.Left,
|
||||
};
|
||||
|
||||
OnboardingTooltip.displayName = 'OnboardingTooltip';
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
WrapEthOnboardingStep2,
|
||||
WrapEthOnboardingStep3,
|
||||
} from 'ts/components/onboarding/wrap_eth_onboarding_step';
|
||||
import { AllowanceToggle } from 'ts/containers/inputs/allowance_toggle';
|
||||
import { AllowanceStateToggle } from 'ts/containers/inputs/allowance_state_toggle';
|
||||
import { BrowserType, ProviderType, ScreenWidths, Token, TokenByAddress, TokenStateByAddress } from 'ts/types';
|
||||
import { analytics } from 'ts/utils/analytics';
|
||||
import { utils } from 'ts/utils/utils';
|
||||
@@ -149,8 +149,8 @@ class PlainPortalOnboardingFlow extends React.Component<PortalOnboardingFlowProp
|
||||
title: 'Step 3: Unlock Tokens',
|
||||
content: (
|
||||
<SetAllowancesOnboardingStep
|
||||
zrxAllowanceToggle={this._renderZrxAllowanceToggle()}
|
||||
ethAllowanceToggle={this._renderEthAllowanceToggle()}
|
||||
zrxAllowanceToggle={this._renderZrxAllowanceStateToggle()}
|
||||
ethAllowanceToggle={this._renderEthAllowanceStateToggle()}
|
||||
doesUserHaveAllowancesForWethAndZrx={this._doesUserHaveAllowancesForWethAndZrx()}
|
||||
/>
|
||||
),
|
||||
@@ -243,15 +243,15 @@ class PlainPortalOnboardingFlow extends React.Component<PortalOnboardingFlowProp
|
||||
stepIndex: this.props.stepIndex,
|
||||
});
|
||||
}
|
||||
private _renderZrxAllowanceToggle(): React.ReactNode {
|
||||
private _renderZrxAllowanceStateToggle(): React.ReactNode {
|
||||
const zrxToken = utils.getZrxToken(this.props.tokenByAddress);
|
||||
return this._renderAllowanceToggle(zrxToken);
|
||||
return this._renderAllowanceStateToggle(zrxToken);
|
||||
}
|
||||
private _renderEthAllowanceToggle(): React.ReactNode {
|
||||
private _renderEthAllowanceStateToggle(): React.ReactNode {
|
||||
const ethToken = utils.getEthToken(this.props.tokenByAddress);
|
||||
return this._renderAllowanceToggle(ethToken);
|
||||
return this._renderAllowanceStateToggle(ethToken);
|
||||
}
|
||||
private _renderAllowanceToggle(token: Token): React.ReactNode {
|
||||
private _renderAllowanceStateToggle(token: Token): React.ReactNode {
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
@@ -260,10 +260,9 @@ class PlainPortalOnboardingFlow extends React.Component<PortalOnboardingFlowProp
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<AllowanceToggle
|
||||
<AllowanceStateToggle
|
||||
token={token}
|
||||
tokenState={tokenStateIfExists}
|
||||
isDisabled={!tokenStateIfExists.isLoaded}
|
||||
blockchain={this.props.blockchain}
|
||||
// tslint:disable-next-line:jsx-no-lambda
|
||||
refetchTokenStateAsync={async () => this.props.refetchTokenStateAsync(token.address)}
|
||||
|
||||
@@ -24,6 +24,7 @@ import { TradeHistory } from 'ts/components/trade_history/trade_history';
|
||||
import { Container } from 'ts/components/ui/container';
|
||||
import { FlashMessage } from 'ts/components/ui/flash_message';
|
||||
import { Image } from 'ts/components/ui/image';
|
||||
import { PointerDirection } from 'ts/components/ui/pointer';
|
||||
import { Text } from 'ts/components/ui/text';
|
||||
import { Wallet } from 'ts/components/wallet/wallet';
|
||||
import { GenerateOrderForm } from 'ts/containers/generate_order_form';
|
||||
@@ -355,6 +356,9 @@ export class Portal extends React.Component<PortalProps, PortalState> {
|
||||
onAddToken={this._onAddToken.bind(this)}
|
||||
onRemoveToken={this._onRemoveToken.bind(this)}
|
||||
refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this)}
|
||||
toggleTooltipDirection={
|
||||
this.props.isPortalOnboardingShowing ? PointerDirection.Left : PointerDirection.Right
|
||||
}
|
||||
/>
|
||||
</Container>
|
||||
{!isMobile && <Container marginTop="8px">{this._renderStartOnboarding()}</Container>}
|
||||
|
||||
@@ -24,7 +24,7 @@ import { SendButton } from 'ts/components/send_button';
|
||||
import { HelpTooltip } from 'ts/components/ui/help_tooltip';
|
||||
import { LifeCycleRaisedButton } from 'ts/components/ui/lifecycle_raised_button';
|
||||
import { TokenIcon } from 'ts/components/ui/token_icon';
|
||||
import { AllowanceToggle } from 'ts/containers/inputs/allowance_toggle';
|
||||
import { AllowanceStateToggle } from 'ts/containers/inputs/allowance_state_toggle';
|
||||
import { trackedTokenStorage } from 'ts/local_storage/tracked_token_storage';
|
||||
import { Dispatcher } from 'ts/redux/dispatcher';
|
||||
import {
|
||||
@@ -372,14 +372,15 @@ export class TokenBalances extends React.Component<TokenBalancesProps, TokenBala
|
||||
)}
|
||||
</TableRowColumn>
|
||||
<TableRowColumn>
|
||||
<AllowanceToggle
|
||||
blockchain={this.props.blockchain}
|
||||
token={token}
|
||||
tokenState={tokenState}
|
||||
onErrorOccurred={this._onErrorOccurred.bind(this)}
|
||||
isDisabled={!tokenState.isLoaded}
|
||||
refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this, token.address)}
|
||||
/>
|
||||
<div className="flex justify-center">
|
||||
<AllowanceStateToggle
|
||||
blockchain={this.props.blockchain}
|
||||
token={token}
|
||||
tokenState={tokenState}
|
||||
onErrorOccurred={this._onErrorOccurred.bind(this)}
|
||||
refetchTokenStateAsync={this._refetchTokenStateAsync.bind(this, token.address)}
|
||||
/>
|
||||
</div>
|
||||
</TableRowColumn>
|
||||
{utils.isTestNetwork(this.props.networkId) && (
|
||||
<TableRowColumn style={{ paddingLeft: actionPaddingX, paddingRight: actionPaddingX }}>
|
||||
|
||||
51
packages/website/ts/components/ui/allowance_state_view.tsx
Normal file
51
packages/website/ts/components/ui/allowance_state_view.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { colors } from '@0xproject/react-shared';
|
||||
import * as React from 'react';
|
||||
import { Container } from 'ts/components/ui/container';
|
||||
import { Spinner } from 'ts/components/ui/spinner';
|
||||
|
||||
export enum AllowanceState {
|
||||
Locked,
|
||||
Unlocked,
|
||||
Loading,
|
||||
}
|
||||
|
||||
export interface AllowanceStateViewProps {
|
||||
allowanceState: AllowanceState;
|
||||
}
|
||||
|
||||
export const AllowanceStateView: React.StatelessComponent<AllowanceStateViewProps> = ({ allowanceState }) => {
|
||||
switch (allowanceState) {
|
||||
case AllowanceState.Locked:
|
||||
return renderLock();
|
||||
case AllowanceState.Unlocked:
|
||||
return renderCheck();
|
||||
case AllowanceState.Loading:
|
||||
return (
|
||||
<Container position="relative" top="3px" left="5px">
|
||||
<Spinner size={18} strokeSize={2} />
|
||||
</Container>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const renderCheck = (color: string = colors.lightGreen) => (
|
||||
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="8.5" cy="8.5" r="8.5" fill={color} />
|
||||
<path
|
||||
d="M2.5 4.5L1.79289 5.20711L2.5 5.91421L3.20711 5.20711L2.5 4.5ZM-0.707107 2.70711L1.79289 5.20711L3.20711 3.79289L0.707107 1.29289L-0.707107 2.70711ZM3.20711 5.20711L7.70711 0.707107L6.29289 -0.707107L1.79289 3.79289L3.20711 5.20711Z"
|
||||
transform="translate(5 6.5)"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
const renderLock = () => (
|
||||
<svg width="12" height="15" viewBox="0 0 12 15" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M6 0C3.51604 0 1.48688 2.0495 1.48688 4.55837V5.86581C0.664723 5.86581 -3.33647e-08 6.53719 -3.33647e-08 7.36759V13.3217C-3.33647e-08 14.1521 0.664723 14.8235 1.48688 14.8235H10.5131C11.3353 14.8235 12 14.1521 12 13.3217V7.36759C12 6.53719 11.3353 5.86581 10.5131 5.86581V4.55837C10.5131 2.0495 8.48396 0 6 0ZM8.93878 5.86581H3.06122V4.55837C3.06122 2.9329 4.37318 1.59013 6 1.59013C7.62682 1.59013 8.93878 2.9329 8.93878 4.55837V5.86581Z"
|
||||
fill="black"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
@@ -32,6 +32,7 @@ export interface ContainerProps {
|
||||
bottom?: string;
|
||||
zIndex?: number;
|
||||
Tag?: ContainerTag;
|
||||
cursor?: string;
|
||||
id?: string;
|
||||
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,12 @@ import { colors } from '@0xproject/react-shared';
|
||||
import * as React from 'react';
|
||||
import { styled } from 'ts/style/theme';
|
||||
|
||||
export type PointerDirection = 'top' | 'right' | 'bottom' | 'left';
|
||||
export enum PointerDirection {
|
||||
Top = 'top',
|
||||
Right = 'right',
|
||||
Bottom = 'bottom',
|
||||
Left = 'left',
|
||||
}
|
||||
|
||||
export interface PointerProps {
|
||||
className?: string;
|
||||
|
||||
54
packages/website/ts/components/ui/spinner.tsx
Normal file
54
packages/website/ts/components/ui/spinner.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { colors } from '@0xproject/react-shared';
|
||||
import * as React from 'react';
|
||||
import { styled } from 'ts/style/theme';
|
||||
|
||||
import { dash, rotate } from 'ts/style/keyframes';
|
||||
|
||||
interface SpinnerSvgProps {
|
||||
color: string;
|
||||
size: number;
|
||||
viewBox?: string;
|
||||
}
|
||||
|
||||
const SpinnerSvg: React.StatelessComponent<SpinnerSvgProps> = props => <svg {...props} />;
|
||||
|
||||
const StyledSpinner = styled(SpinnerSvg)`
|
||||
animation: ${rotate} 3s linear infinite;
|
||||
margin: ${props => `-${props.size / 2}px 0 0 -${props.size / 2}px`};
|
||||
margin-top: ${props => `-${props.size / 2}px`};
|
||||
margin-left: ${props => `-${props.size / 2}px`};
|
||||
margin-bottom: 0px;
|
||||
margin-right: 0px;
|
||||
size: ${props => `${props.size}px`};
|
||||
height: ${props => `${props.size}px`};
|
||||
|
||||
& .path {
|
||||
stroke: ${props => props.color};
|
||||
stroke-linecap: round;
|
||||
animation: ${dash} 2.5s ease-in-out infinite;
|
||||
}
|
||||
`;
|
||||
|
||||
export interface SpinnerProps {
|
||||
size?: number;
|
||||
strokeSize?: number;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export const Spinner: React.StatelessComponent<SpinnerProps> = ({ size, strokeSize, color }) => {
|
||||
const c = size / 2;
|
||||
const r = c - strokeSize;
|
||||
return (
|
||||
<StyledSpinner color={color} size={size} viewBox={`0 0 ${size} ${size}`}>
|
||||
<circle className="path" cx={c} cy={c} r={r} fill="none" strokeWidth={strokeSize} />
|
||||
</StyledSpinner>
|
||||
);
|
||||
};
|
||||
|
||||
Spinner.defaultProps = {
|
||||
size: 50,
|
||||
color: colors.mediumBlue,
|
||||
strokeSize: 4,
|
||||
};
|
||||
|
||||
Spinner.displayName = 'Spinner';
|
||||
@@ -19,6 +19,7 @@ export interface TextProps {
|
||||
textDecorationLine?: string;
|
||||
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
|
||||
hoverColor?: string;
|
||||
noWrap?: boolean;
|
||||
}
|
||||
|
||||
const PlainText: React.StatelessComponent<TextProps> = ({ children, className, onClick, Tag }) => (
|
||||
@@ -39,6 +40,7 @@ export const Text = styled(PlainText)`
|
||||
${props => (props.minHeight ? `min-height: ${props.minHeight}` : '')};
|
||||
${props => (props.onClick ? 'cursor: pointer' : '')};
|
||||
transition: color 0.5s ease;
|
||||
${props => (props.noWrap ? 'white-space: nowrap' : '')};
|
||||
&:hover {
|
||||
${props => (props.onClick ? `color: ${props.hoverColor || darken(0.3, props.fontColor)}` : '')};
|
||||
}
|
||||
@@ -53,6 +55,7 @@ Text.defaultProps = {
|
||||
lineHeight: '1.5em',
|
||||
textDecorationLine: 'none',
|
||||
Tag: 'div',
|
||||
noWrap: false,
|
||||
};
|
||||
|
||||
Text.displayName = 'Text';
|
||||
|
||||
@@ -14,6 +14,7 @@ import { DropDown, DropdownMouseEvent } from 'ts/components/ui/drop_down';
|
||||
import { IconButton } from 'ts/components/ui/icon_button';
|
||||
import { Identicon } from 'ts/components/ui/identicon';
|
||||
import { Island } from 'ts/components/ui/island';
|
||||
import { PointerDirection } from 'ts/components/ui/pointer';
|
||||
import {
|
||||
CopyAddressSimpleMenuItem,
|
||||
DifferentWalletSimpleMenuItem,
|
||||
@@ -28,7 +29,7 @@ import { NullTokenRow } from 'ts/components/wallet/null_token_row';
|
||||
import { PlaceHolder } from 'ts/components/wallet/placeholder';
|
||||
import { StandardIconRow } from 'ts/components/wallet/standard_icon_row';
|
||||
import { WrapEtherItem } from 'ts/components/wallet/wrap_ether_item';
|
||||
import { AllowanceToggle } from 'ts/containers/inputs/allowance_toggle';
|
||||
import { AllowanceStateToggle } from 'ts/containers/inputs/allowance_state_toggle';
|
||||
import { Dispatcher } from 'ts/redux/dispatcher';
|
||||
import { colors } from 'ts/style/colors';
|
||||
import {
|
||||
@@ -67,6 +68,7 @@ export interface WalletProps {
|
||||
onRemoveToken: () => void;
|
||||
refetchTokenStateAsync: (tokenAddress: string) => Promise<void>;
|
||||
style: React.CSSProperties;
|
||||
toggleTooltipDirection?: PointerDirection;
|
||||
}
|
||||
|
||||
interface WalletState {
|
||||
@@ -74,14 +76,14 @@ interface WalletState {
|
||||
isHoveringSidebar: boolean;
|
||||
}
|
||||
|
||||
interface AllowanceToggleConfig {
|
||||
interface AllowanceStateToggleConfig {
|
||||
token: Token;
|
||||
tokenState: TokenState;
|
||||
}
|
||||
|
||||
interface AccessoryItemConfig {
|
||||
wrappedEtherDirection?: Side;
|
||||
allowanceToggleConfig?: AllowanceToggleConfig;
|
||||
allowanceStateToggleConfig?: AllowanceStateToggleConfig;
|
||||
}
|
||||
|
||||
const ETHER_ICON_PATH = '/images/ether.png';
|
||||
@@ -89,7 +91,8 @@ const ICON_DIMENSION = 28;
|
||||
const BODY_ITEM_KEY = 'BODY';
|
||||
const HEADER_ITEM_KEY = 'HEADER';
|
||||
const ETHER_ITEM_KEY = 'ETHER';
|
||||
const NO_ALLOWANCE_TOGGLE_SPACE_WIDTH = 56;
|
||||
const WRAP_ROW_ALLOWANCE_TOGGLE_WIDTH = 67;
|
||||
const ALLOWANCE_TOGGLE_WIDTH = 56;
|
||||
const PLACEHOLDER_COLOR = colors.grey300;
|
||||
const LOADING_ROWS_COUNT = 6;
|
||||
|
||||
@@ -338,7 +341,7 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
|
||||
);
|
||||
const accessoryItemConfig: AccessoryItemConfig = {
|
||||
wrappedEtherDirection,
|
||||
allowanceToggleConfig: {
|
||||
allowanceStateToggleConfig: {
|
||||
token,
|
||||
tokenState,
|
||||
},
|
||||
@@ -393,13 +396,15 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
|
||||
}
|
||||
private _renderAccessoryItems(config: AccessoryItemConfig): React.ReactElement<{}> {
|
||||
const shouldShowWrappedEtherAction = !_.isUndefined(config.wrappedEtherDirection);
|
||||
const shouldShowToggle = !_.isUndefined(config.allowanceToggleConfig);
|
||||
const shouldShowToggle = !_.isUndefined(config.allowanceStateToggleConfig);
|
||||
// if we don't have a toggle, we still want some space to the right of the "wrap" button so that it aligns with
|
||||
// the "unwrap" button in the row below
|
||||
const toggle = shouldShowToggle ? (
|
||||
this._renderAllowanceToggle(config.allowanceToggleConfig)
|
||||
) : (
|
||||
<div style={{ width: NO_ALLOWANCE_TOGGLE_SPACE_WIDTH }} />
|
||||
const isWrapEtherRow = shouldShowWrappedEtherAction && config.wrappedEtherDirection === Side.Deposit;
|
||||
const width = isWrapEtherRow ? WRAP_ROW_ALLOWANCE_TOGGLE_WIDTH : ALLOWANCE_TOGGLE_WIDTH;
|
||||
const toggle = (
|
||||
<Container className="flex justify-center" width={width}>
|
||||
{shouldShowToggle && this._renderAllowanceToggle(config.allowanceStateToggleConfig)}
|
||||
</Container>
|
||||
);
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
@@ -410,14 +415,14 @@ export class Wallet extends React.Component<WalletProps, WalletState> {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
private _renderAllowanceToggle(config: AllowanceToggleConfig): React.ReactNode {
|
||||
private _renderAllowanceToggle(config: AllowanceStateToggleConfig): React.ReactNode {
|
||||
// TODO: Error handling
|
||||
return (
|
||||
<AllowanceToggle
|
||||
<AllowanceStateToggle
|
||||
blockchain={this.props.blockchain}
|
||||
token={config.token}
|
||||
tokenState={config.tokenState}
|
||||
isDisabled={!config.tokenState.isLoaded}
|
||||
tooltipDirection={this.props.toggleTooltipDirection}
|
||||
refetchTokenStateAsync={async () => this.props.refetchTokenStateAsync(config.token.address)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -2,19 +2,20 @@ import * as React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Dispatch } from 'redux';
|
||||
import { Blockchain } from 'ts/blockchain';
|
||||
import { PointerDirection } from 'ts/components/ui/pointer';
|
||||
import { State } from 'ts/redux/reducer';
|
||||
import { BalanceErrs, Token, TokenState } from 'ts/types';
|
||||
|
||||
import { AllowanceToggle as AllowanceToggleComponent } from 'ts/components/inputs/allowance_toggle';
|
||||
import { AllowanceStateToggle as AllowanceStateToggleComponent } from 'ts/components/inputs/allowance_state_toggle';
|
||||
import { Dispatcher } from 'ts/redux/dispatcher';
|
||||
|
||||
interface AllowanceToggleProps {
|
||||
interface AllowanceStateToggleProps {
|
||||
blockchain: Blockchain;
|
||||
onErrorOccurred?: (errType: BalanceErrs) => void;
|
||||
token: Token;
|
||||
tokenState: TokenState;
|
||||
isDisabled?: boolean;
|
||||
refetchTokenStateAsync: () => Promise<void>;
|
||||
tooltipDirection?: PointerDirection;
|
||||
}
|
||||
|
||||
interface ConnectedState {
|
||||
@@ -26,7 +27,7 @@ interface ConnectedDispatch {
|
||||
dispatcher: Dispatcher;
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: State, _ownProps: AllowanceToggleProps): ConnectedState => ({
|
||||
const mapStateToProps = (state: State, _ownProps: AllowanceStateToggleProps): ConnectedState => ({
|
||||
networkId: state.networkId,
|
||||
userAddress: state.userAddress,
|
||||
});
|
||||
@@ -35,7 +36,7 @@ const mapDispatchTopProps = (dispatch: Dispatch<State>): ConnectedDispatch => ({
|
||||
dispatcher: new Dispatcher(dispatch),
|
||||
});
|
||||
|
||||
export const AllowanceToggle: React.ComponentClass<AllowanceToggleProps> = connect(
|
||||
export const AllowanceStateToggle: React.ComponentClass<AllowanceStateToggleProps> = connect(
|
||||
mapStateToProps,
|
||||
mapDispatchTopProps,
|
||||
)(AllowanceToggleComponent);
|
||||
)(AllowanceStateToggleComponent);
|
||||
22
packages/website/ts/style/keyframes.ts
Normal file
22
packages/website/ts/style/keyframes.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { keyframes } from 'ts/style/theme';
|
||||
|
||||
export const rotate = keyframes`
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
`;
|
||||
|
||||
export const dash = keyframes`
|
||||
0% {
|
||||
stroke-dasharray: 1, 150;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
50% {
|
||||
stroke-dasharray: 90, 150;
|
||||
stroke-dashoffset: -35;
|
||||
}
|
||||
100% {
|
||||
stroke-dasharray: 90, 150;
|
||||
stroke-dashoffset: -124;
|
||||
}
|
||||
`;
|
||||
Reference in New Issue
Block a user