Merge pull request #1170 from 0xProject/feature/instant/failure-state
[instant] Failure state
This commit is contained in:
commit
8635f8d732
@ -6,7 +6,7 @@ import { ColorOption } from '../style/theme';
|
|||||||
import { util } from '../util/util';
|
import { util } from '../util/util';
|
||||||
import { web3Wrapper } from '../util/web3_wrapper';
|
import { web3Wrapper } from '../util/web3_wrapper';
|
||||||
|
|
||||||
import { Button, Container, Text } from './ui';
|
import { Button, Text } from './ui';
|
||||||
|
|
||||||
export interface BuyButtonProps {
|
export interface BuyButtonProps {
|
||||||
buyQuote?: BuyQuote;
|
buyQuote?: BuyQuote;
|
||||||
@ -14,7 +14,6 @@ export interface BuyButtonProps {
|
|||||||
onClick: (buyQuote: BuyQuote) => void;
|
onClick: (buyQuote: BuyQuote) => void;
|
||||||
onBuySuccess: (buyQuote: BuyQuote, txnHash: string) => void;
|
onBuySuccess: (buyQuote: BuyQuote, txnHash: string) => void;
|
||||||
onBuyFailure: (buyQuote: BuyQuote, tnxHash?: string) => void;
|
onBuyFailure: (buyQuote: BuyQuote, tnxHash?: string) => void;
|
||||||
text: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BuyButton extends React.Component<BuyButtonProps> {
|
export class BuyButton extends React.Component<BuyButtonProps> {
|
||||||
@ -26,13 +25,11 @@ export class BuyButton extends React.Component<BuyButtonProps> {
|
|||||||
public render(): React.ReactNode {
|
public render(): React.ReactNode {
|
||||||
const shouldDisableButton = _.isUndefined(this.props.buyQuote) || _.isUndefined(this.props.assetBuyer);
|
const shouldDisableButton = _.isUndefined(this.props.buyQuote) || _.isUndefined(this.props.assetBuyer);
|
||||||
return (
|
return (
|
||||||
<Container padding="20px" width="100%">
|
<Button width="100%" onClick={this._handleClick} isDisabled={shouldDisableButton}>
|
||||||
<Button width="100%" onClick={this._handleClick} isDisabled={shouldDisableButton}>
|
<Text fontColor={ColorOption.white} fontWeight={600} fontSize="20px">
|
||||||
<Text fontColor={ColorOption.white} fontWeight={600} fontSize="20px">
|
Buy
|
||||||
{this.props.text}
|
</Text>
|
||||||
</Text>
|
</Button>
|
||||||
</Button>
|
|
||||||
</Container>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
private readonly _handleClick = async () => {
|
private readonly _handleClick = async () => {
|
||||||
|
24
packages/instant/src/components/buy_order_state_button.tsx
Normal file
24
packages/instant/src/components/buy_order_state_button.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { SelectedAssetBuyButton } from '../containers/selected_asset_buy_button';
|
||||||
|
import { SelectedAssetRetryButton } from '../containers/selected_asset_retry_button';
|
||||||
|
|
||||||
|
import { AsyncProcessState } from '../types';
|
||||||
|
|
||||||
|
import { SecondaryButton } from './secondary_button';
|
||||||
|
|
||||||
|
export interface BuyOrderStateButtonProps {
|
||||||
|
buyOrderState: AsyncProcessState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BuyOrderStateButton: React.StatelessComponent<BuyOrderStateButtonProps> = props => {
|
||||||
|
if (props.buyOrderState === AsyncProcessState.FAILURE) {
|
||||||
|
return <SelectedAssetRetryButton />;
|
||||||
|
} else if (props.buyOrderState === AsyncProcessState.SUCCESS) {
|
||||||
|
return <SecondaryButton text="Success" isDisabled={true} />;
|
||||||
|
} else if (props.buyOrderState === AsyncProcessState.PENDING) {
|
||||||
|
return <SecondaryButton text="Processing" isDisabled={true} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <SelectedAssetBuyButton />;
|
||||||
|
};
|
@ -9,17 +9,20 @@ import { format } from '../util/format';
|
|||||||
|
|
||||||
import { AmountPlaceholder } from './amount_placeholder';
|
import { AmountPlaceholder } from './amount_placeholder';
|
||||||
import { Container, Flex, Text } from './ui';
|
import { Container, Flex, Text } from './ui';
|
||||||
|
import { Icon } from './ui/icon';
|
||||||
|
|
||||||
export interface InstantHeadingProps {
|
export interface InstantHeadingProps {
|
||||||
selectedAssetAmount?: BigNumber;
|
selectedAssetAmount?: BigNumber;
|
||||||
totalEthBaseAmount?: BigNumber;
|
totalEthBaseAmount?: BigNumber;
|
||||||
ethUsdPrice?: BigNumber;
|
ethUsdPrice?: BigNumber;
|
||||||
quoteRequestState: AsyncProcessState;
|
quoteRequestState: AsyncProcessState;
|
||||||
|
buyOrderState: AsyncProcessState;
|
||||||
}
|
}
|
||||||
|
|
||||||
const placeholderColor = ColorOption.white;
|
const placeholderColor = ColorOption.white;
|
||||||
export class InstantHeading extends React.Component<InstantHeadingProps, {}> {
|
export class InstantHeading extends React.Component<InstantHeadingProps, {}> {
|
||||||
public render(): React.ReactNode {
|
public render(): React.ReactNode {
|
||||||
|
const iconOrAmounts = this._renderIcon() || this._renderAmountsSection();
|
||||||
return (
|
return (
|
||||||
<Container
|
<Container
|
||||||
backgroundColor={ColorOption.primaryColor}
|
backgroundColor={ColorOption.primaryColor}
|
||||||
@ -36,20 +39,43 @@ export class InstantHeading extends React.Component<InstantHeadingProps, {}> {
|
|||||||
textTransform="uppercase"
|
textTransform="uppercase"
|
||||||
fontSize="12px"
|
fontSize="12px"
|
||||||
>
|
>
|
||||||
I want to buy
|
{this._renderTopText()}
|
||||||
</Text>
|
</Text>
|
||||||
</Container>
|
</Container>
|
||||||
<Flex direction="row" justify="space-between">
|
<Flex direction="row" justify="space-between">
|
||||||
<SelectedAssetAmountInput fontSize="45px" />
|
<SelectedAssetAmountInput fontSize="45px" />
|
||||||
<Flex direction="column" justify="space-between">
|
<Flex direction="column" justify="space-between">
|
||||||
<Container marginBottom="5px">{this._placeholderOrAmount(this._ethAmount)}</Container>
|
{iconOrAmounts}
|
||||||
<Container opacity={0.7}>{this._placeholderOrAmount(this._dollarAmount)}</Container>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _renderAmountsSection(): React.ReactNode {
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Container marginBottom="5px">{this._placeholderOrAmount(this._ethAmount)}</Container>
|
||||||
|
<Container opacity={0.7}>{this._placeholderOrAmount(this._dollarAmount)}</Container>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderIcon(): React.ReactNode {
|
||||||
|
if (this.props.buyOrderState === AsyncProcessState.FAILURE) {
|
||||||
|
return <Icon icon={'failed'} width={34} height={34} color={ColorOption.white} />;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderTopText(): React.ReactNode {
|
||||||
|
if (this.props.buyOrderState === AsyncProcessState.FAILURE) {
|
||||||
|
return 'Order failed';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'I want to buy';
|
||||||
|
}
|
||||||
|
|
||||||
private _placeholderOrAmount(amountFunction: () => React.ReactNode): React.ReactNode {
|
private _placeholderOrAmount(amountFunction: () => React.ReactNode): React.ReactNode {
|
||||||
if (this.props.quoteRequestState === AsyncProcessState.PENDING) {
|
if (this.props.quoteRequestState === AsyncProcessState.PENDING) {
|
||||||
return <AmountPlaceholder isPulsating={true} color={placeholderColor} />;
|
return <AmountPlaceholder isPulsating={true} color={placeholderColor} />;
|
||||||
|
11
packages/instant/src/components/retry_button.tsx
Normal file
11
packages/instant/src/components/retry_button.tsx
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { SecondaryButton } from './secondary_button';
|
||||||
|
|
||||||
|
export interface RetryButtonProps {
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RetryButton: React.StatelessComponent<RetryButtonProps> = props => {
|
||||||
|
return <SecondaryButton text="Try Again" onClick={props.onClick} />;
|
||||||
|
};
|
28
packages/instant/src/components/secondary_button.tsx
Normal file
28
packages/instant/src/components/secondary_button.tsx
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import * as _ from 'lodash';
|
||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { ColorOption } from '../style/theme';
|
||||||
|
|
||||||
|
import { Button, ButtonProps } from './ui/button';
|
||||||
|
import { Text } from './ui/text';
|
||||||
|
|
||||||
|
export interface SecondaryButtonProps extends ButtonProps {
|
||||||
|
text: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SecondaryButton: React.StatelessComponent<SecondaryButtonProps> = props => {
|
||||||
|
const buttonProps = _.omit(props, 'text');
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
backgroundColor={ColorOption.white}
|
||||||
|
borderColor={ColorOption.lightGrey}
|
||||||
|
width="100%"
|
||||||
|
onClick={props.onClick}
|
||||||
|
{...buttonProps}
|
||||||
|
>
|
||||||
|
<Text fontColor={ColorOption.primaryColor} fontWeight={600} fontSize="16px">
|
||||||
|
{props.text}
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
58
packages/instant/src/components/ui/icon.tsx
Normal file
58
packages/instant/src/components/ui/icon.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { ColorOption } from '../../style/theme';
|
||||||
|
|
||||||
|
type svgRule = 'evenodd' | 'nonzero' | 'inherit';
|
||||||
|
interface IconInfo {
|
||||||
|
viewBox: string;
|
||||||
|
fillRule?: svgRule;
|
||||||
|
clipRule?: svgRule;
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
interface IconInfoMapping {
|
||||||
|
failed: IconInfo;
|
||||||
|
success: IconInfo;
|
||||||
|
}
|
||||||
|
const ICONS: IconInfoMapping = {
|
||||||
|
failed: {
|
||||||
|
viewBox: '0 0 34 34',
|
||||||
|
fillRule: 'evenodd',
|
||||||
|
clipRule: 'evenodd',
|
||||||
|
path:
|
||||||
|
'M6.65771 26.4362C9.21777 29.2406 12.9033 31 17 31C24.7319 31 31 24.7319 31 17C31 14.4468 30.3164 12.0531 29.1226 9.99219L6.65771 26.4362ZM4.88281 24.0173C3.68555 21.9542 3 19.5571 3 17C3 9.26807 9.26807 3 17 3C21.1006 3 24.7891 4.76294 27.3496 7.57214L4.88281 24.0173ZM0 17C0 26.3888 7.61133 34 17 34C26.3887 34 34 26.3888 34 17C34 7.61121 26.3887 0 17 0C7.61133 0 0 7.61121 0 17Z',
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
viewBox: '0 0 34 34',
|
||||||
|
fillRule: 'evenodd',
|
||||||
|
clipRule: 'evenodd',
|
||||||
|
path:
|
||||||
|
'M17 34C26.3887 34 34 26.3888 34 17C34 7.61121 26.3887 0 17 0C7.61133 0 0 7.61121 0 17C0 26.3888 7.61133 34 17 34ZM25.7539 13.0977C26.2969 12.4718 26.2295 11.5244 25.6035 10.9817C24.9775 10.439 24.0303 10.5063 23.4878 11.1323L15.731 20.0771L12.3936 16.7438C11.8071 16.1583 10.8574 16.1589 10.272 16.7451C9.68652 17.3313 9.6875 18.281 10.2734 18.8665L14.75 23.3373L15.8887 24.4746L16.9434 23.2587L25.7539 13.0977Z',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface IconProps {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
color: ColorOption;
|
||||||
|
icon: keyof IconInfoMapping;
|
||||||
|
}
|
||||||
|
export const Icon: React.SFC<IconProps> = props => {
|
||||||
|
const iconInfo = ICONS[props.icon];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
width={props.width}
|
||||||
|
height={props.height}
|
||||||
|
viewBox={iconInfo.viewBox}
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d={iconInfo.path}
|
||||||
|
fill={props.color}
|
||||||
|
fillRule={iconInfo.fillRule || 'nonzero'}
|
||||||
|
clipRule={iconInfo.clipRule || 'nonzero'}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
@ -2,7 +2,7 @@ import * as React from 'react';
|
|||||||
|
|
||||||
import { LatestBuyQuoteOrderDetails } from '../containers/latest_buy_quote_order_details';
|
import { LatestBuyQuoteOrderDetails } from '../containers/latest_buy_quote_order_details';
|
||||||
import { LatestError } from '../containers/latest_error';
|
import { LatestError } from '../containers/latest_error';
|
||||||
import { SelectedAssetBuyButton } from '../containers/selected_asset_buy_button';
|
import { SelectedAssetBuyOrderStateButton } from '../containers/selected_asset_buy_order_state_button';
|
||||||
import { SelectedAssetInstantHeading } from '../containers/selected_asset_instant_heading';
|
import { SelectedAssetInstantHeading } from '../containers/selected_asset_instant_heading';
|
||||||
|
|
||||||
import { ColorOption } from '../style/theme';
|
import { ColorOption } from '../style/theme';
|
||||||
@ -26,7 +26,9 @@ export const ZeroExInstantContainer: React.StatelessComponent<ZeroExInstantConta
|
|||||||
<Flex direction="column" justify="flex-start">
|
<Flex direction="column" justify="flex-start">
|
||||||
<SelectedAssetInstantHeading />
|
<SelectedAssetInstantHeading />
|
||||||
<LatestBuyQuoteOrderDetails />
|
<LatestBuyQuoteOrderDetails />
|
||||||
<SelectedAssetBuyButton />
|
<Container padding="20px" width="100%">
|
||||||
|
<SelectedAssetBuyOrderStateButton />
|
||||||
|
</Container>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Container>
|
</Container>
|
||||||
</Container>
|
</Container>
|
||||||
|
@ -14,7 +14,6 @@ export interface SelectedAssetBuyButtonProps {}
|
|||||||
|
|
||||||
interface ConnectedState {
|
interface ConnectedState {
|
||||||
assetBuyer?: AssetBuyer;
|
assetBuyer?: AssetBuyer;
|
||||||
text: string;
|
|
||||||
buyQuote?: BuyQuote;
|
buyQuote?: BuyQuote;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,24 +23,8 @@ interface ConnectedDispatch {
|
|||||||
onBuyFailure: (buyQuote: BuyQuote) => void;
|
onBuyFailure: (buyQuote: BuyQuote) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const textForState = (state: AsyncProcessState): string => {
|
|
||||||
switch (state) {
|
|
||||||
case AsyncProcessState.NONE:
|
|
||||||
return 'Buy';
|
|
||||||
case AsyncProcessState.PENDING:
|
|
||||||
return '...Loading';
|
|
||||||
case AsyncProcessState.SUCCESS:
|
|
||||||
return 'Success!';
|
|
||||||
case AsyncProcessState.FAILURE:
|
|
||||||
return 'Failed';
|
|
||||||
default:
|
|
||||||
return 'Buy';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapStateToProps = (state: State, _ownProps: SelectedAssetBuyButtonProps): ConnectedState => ({
|
const mapStateToProps = (state: State, _ownProps: SelectedAssetBuyButtonProps): ConnectedState => ({
|
||||||
assetBuyer: state.assetBuyer,
|
assetBuyer: state.assetBuyer,
|
||||||
text: textForState(state.buyOrderState),
|
|
||||||
buyQuote: state.latestBuyQuote,
|
buyQuote: state.latestBuyQuote,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
import * as _ from 'lodash';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import { State } from '../redux/reducer';
|
||||||
|
import { AsyncProcessState } from '../types';
|
||||||
|
|
||||||
|
import { BuyOrderStateButton } from '../components/buy_order_state_button';
|
||||||
|
|
||||||
|
interface ConnectedState {
|
||||||
|
buyOrderState: AsyncProcessState;
|
||||||
|
}
|
||||||
|
export interface SelectedAssetButtonProps {}
|
||||||
|
const mapStateToProps = (state: State, _ownProps: SelectedAssetButtonProps): ConnectedState => ({
|
||||||
|
buyOrderState: state.buyOrderState,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SelectedAssetBuyOrderStateButton: React.ComponentClass<SelectedAssetButtonProps> = connect(
|
||||||
|
mapStateToProps,
|
||||||
|
)(BuyOrderStateButton);
|
@ -16,6 +16,7 @@ interface ConnectedState {
|
|||||||
totalEthBaseAmount?: BigNumber;
|
totalEthBaseAmount?: BigNumber;
|
||||||
ethUsdPrice?: BigNumber;
|
ethUsdPrice?: BigNumber;
|
||||||
quoteRequestState: AsyncProcessState;
|
quoteRequestState: AsyncProcessState;
|
||||||
|
buyOrderState: AsyncProcessState;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state: State, _ownProps: InstantHeadingProps): ConnectedState => ({
|
const mapStateToProps = (state: State, _ownProps: InstantHeadingProps): ConnectedState => ({
|
||||||
@ -23,6 +24,7 @@ const mapStateToProps = (state: State, _ownProps: InstantHeadingProps): Connecte
|
|||||||
totalEthBaseAmount: oc(state).latestBuyQuote.worstCaseQuoteInfo.totalEthAmount(),
|
totalEthBaseAmount: oc(state).latestBuyQuote.worstCaseQuoteInfo.totalEthAmount(),
|
||||||
ethUsdPrice: state.ethUsdPrice,
|
ethUsdPrice: state.ethUsdPrice,
|
||||||
quoteRequestState: state.quoteRequestState,
|
quoteRequestState: state.quoteRequestState,
|
||||||
|
buyOrderState: state.buyOrderState,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const SelectedAssetInstantHeading: React.ComponentClass<InstantHeadingProps> = connect(mapStateToProps)(
|
export const SelectedAssetInstantHeading: React.ComponentClass<InstantHeadingProps> = connect(mapStateToProps)(
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
import * as _ from 'lodash';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { Dispatch } from 'redux';
|
||||||
|
|
||||||
|
import { Action, actions } from '../redux/actions';
|
||||||
|
|
||||||
|
import { RetryButton } from '../components/retry_button';
|
||||||
|
|
||||||
|
export interface SelectedAssetRetryButtonProps {}
|
||||||
|
|
||||||
|
interface ConnectedDispatch {
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = (
|
||||||
|
dispatch: Dispatch<Action>,
|
||||||
|
_ownProps: SelectedAssetRetryButtonProps,
|
||||||
|
): ConnectedDispatch => ({
|
||||||
|
onClick: () => dispatch(actions.resetAmount()),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SelectedAssetRetryButton: React.ComponentClass<SelectedAssetRetryButtonProps> = connect(
|
||||||
|
undefined,
|
||||||
|
mapDispatchToProps,
|
||||||
|
)(RetryButton);
|
@ -31,6 +31,7 @@ export enum ActionTypes {
|
|||||||
SET_ERROR = 'SET_ERROR',
|
SET_ERROR = 'SET_ERROR',
|
||||||
HIDE_ERROR = 'HIDE_ERROR',
|
HIDE_ERROR = 'HIDE_ERROR',
|
||||||
CLEAR_ERROR = 'CLEAR_ERROR',
|
CLEAR_ERROR = 'CLEAR_ERROR',
|
||||||
|
RESET_AMOUNT = 'RESET_AMOUNT',
|
||||||
}
|
}
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
@ -45,4 +46,5 @@ export const actions = {
|
|||||||
setError: (error?: any) => createAction(ActionTypes.SET_ERROR, error),
|
setError: (error?: any) => createAction(ActionTypes.SET_ERROR, error),
|
||||||
hideError: () => createAction(ActionTypes.HIDE_ERROR),
|
hideError: () => createAction(ActionTypes.HIDE_ERROR),
|
||||||
clearError: () => createAction(ActionTypes.CLEAR_ERROR),
|
clearError: () => createAction(ActionTypes.CLEAR_ERROR),
|
||||||
|
resetAmount: () => createAction(ActionTypes.RESET_AMOUNT),
|
||||||
};
|
};
|
||||||
|
@ -101,6 +101,14 @@ export const reducer = (state: State = INITIAL_STATE, action: Action): State =>
|
|||||||
...state,
|
...state,
|
||||||
selectedAsset: newSelectedAsset,
|
selectedAsset: newSelectedAsset,
|
||||||
};
|
};
|
||||||
|
case ActionTypes.RESET_AMOUNT:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
latestBuyQuote: undefined,
|
||||||
|
quoteRequestState: AsyncProcessState.NONE,
|
||||||
|
buyOrderState: AsyncProcessState.NONE,
|
||||||
|
selectedAssetAmount: undefined,
|
||||||
|
};
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user