feat(instant): implement affiliateFeeInfo prop

This commit is contained in:
Brandon Millman
2018-11-01 18:24:32 -07:00
parent 4fda2a2d04
commit 5e66cc8a40
9 changed files with 61 additions and 15 deletions

View File

@@ -1,10 +1,11 @@
import { AssetBuyer, AssetBuyerError, BuyQuote } from '@0x/asset-buyer';
import * as _ from 'lodash';
import * as React from 'react';
import { oc } from 'ts-optchain';
import { WEB_3_WRAPPER_TRANSACTION_FAILED_ERROR_MSG_PREFIX } from '../constants';
import { ColorOption } from '../style/theme';
import { ZeroExInstantError } from '../types';
import { AffiliateInfo, ZeroExInstantError } from '../types';
import { getBestAddress } from '../util/address';
import { balanceUtil } from '../util/balance';
import { gasPriceEstimator } from '../util/gas_price_estimator';
@@ -16,6 +17,7 @@ import { Button, Text } from './ui';
export interface BuyButtonProps {
buyQuote?: BuyQuote;
assetBuyer?: AssetBuyer;
affiliateInfo?: AffiliateInfo;
onValidationPending: (buyQuote: BuyQuote) => void;
onValidationFail: (buyQuote: BuyQuote, errorMessage: AssetBuyerError | ZeroExInstantError) => void;
onSignatureDenied: (buyQuote: BuyQuote) => void;
@@ -42,7 +44,7 @@ export class BuyButton extends React.Component<BuyButtonProps> {
}
private readonly _handleClick = async () => {
// The button is disabled when there is no buy quote anyway.
const { buyQuote, assetBuyer } = this.props;
const { buyQuote, assetBuyer, affiliateInfo } = this.props;
if (_.isUndefined(buyQuote) || _.isUndefined(assetBuyer)) {
return;
}
@@ -58,8 +60,13 @@ export class BuyButton extends React.Component<BuyButtonProps> {
let txHash: string | undefined;
const gasInfo = await gasPriceEstimator.getGasInfoAsync();
const feeRecipient = oc(affiliateInfo).feeRecipient();
try {
txHash = await assetBuyer.executeBuyQuoteAsync(buyQuote, { takerAddress, gasPrice: gasInfo.gasPriceInWei });
txHash = await assetBuyer.executeBuyQuoteAsync(buyQuote, {
feeRecipient,
takerAddress,
gasPrice: gasInfo.gasPriceInWei,
});
} catch (e) {
if (e instanceof Error) {
if (e.message === AssetBuyerError.SignatureRequestDenied) {

View File

@@ -2,7 +2,7 @@ import { AssetBuyer, AssetBuyerError, BuyQuote } from '@0x/asset-buyer';
import * as React from 'react';
import { ColorOption } from '../style/theme';
import { OrderProcessState, ZeroExInstantError } from '../types';
import { AffiliateInfo, OrderProcessState, ZeroExInstantError } from '../types';
import { BuyButton } from './buy_button';
import { PlacingOrderButton } from './placing_order_button';
@@ -13,6 +13,7 @@ export interface BuyOrderStateButtonProps {
buyQuote?: BuyQuote;
buyOrderProcessingState: OrderProcessState;
assetBuyer?: AssetBuyer;
affiliateInfo?: AffiliateInfo;
onViewTransaction: () => void;
onValidationPending: (buyQuote: BuyQuote) => void;
onValidationFail: (buyQuote: BuyQuote, errorMessage: AssetBuyerError | ZeroExInstantError) => void;
@@ -50,6 +51,7 @@ export const BuyOrderStateButtons: React.StatelessComponent<BuyOrderStateButtonP
<BuyButton
buyQuote={props.buyQuote}
assetBuyer={props.assetBuyer}
affiliateInfo={props.affiliateInfo}
onValidationPending={props.onValidationPending}
onValidationFail={props.onValidationFail}
onSignatureDenied={props.onSignatureDenied}

View File

@@ -9,7 +9,7 @@ import { asyncData } from '../redux/async_data';
import { INITIAL_STATE, State } from '../redux/reducer';
import { store, Store } from '../redux/store';
import { fonts } from '../style/fonts';
import { AssetMetaData, Network } from '../types';
import { AffiliateInfo, AssetMetaData, Network } from '../types';
import { assetUtils } from '../util/asset';
import { BigNumberInput } from '../util/big_number_input';
import { errorFlasher } from '../util/error_flasher';
@@ -29,9 +29,10 @@ export interface ZeroExInstantProviderRequiredProps {
}
export interface ZeroExInstantProviderOptionalProps {
defaultAssetBuyAmount?: number;
defaultAssetBuyAmount: number;
additionalAssetMetaDataMap: ObjectMap<AssetMetaData>;
networkId: Network;
affiliateInfo: AffiliateInfo;
}
export class ZeroExInstantProvider extends React.Component<ZeroExInstantProviderProps> {
@@ -66,6 +67,7 @@ export class ZeroExInstantProvider extends React.Component<ZeroExInstantProvider
? state.selectedAssetAmount
: new BigNumberInput(props.defaultAssetBuyAmount),
assetMetaDataMap: completeAssetMetaDataMap,
affiliateInfo: props.affiliateInfo,
};
return storeStateFromProps;
}

View File

@@ -7,7 +7,7 @@ import { Dispatch } from 'redux';
import { BuyOrderStateButtons } from '../components/buy_order_state_buttons';
import { Action, actions } from '../redux/actions';
import { State } from '../redux/reducer';
import { OrderProcessState, ZeroExInstantError } from '../types';
import { AffiliateInfo, OrderProcessState, ZeroExInstantError } from '../types';
import { errorFlasher } from '../util/error_flasher';
import { etherscanUtil } from '../util/etherscan';
@@ -15,6 +15,7 @@ interface ConnectedState {
buyQuote?: BuyQuote;
buyOrderProcessingState: OrderProcessState;
assetBuyer?: AssetBuyer;
affiliateInfo?: AffiliateInfo;
onViewTransaction: () => void;
}
@@ -32,6 +33,7 @@ const mapStateToProps = (state: State, _ownProps: SelectedAssetBuyOrderStateButt
buyOrderProcessingState: state.buyOrderState.processState,
assetBuyer: state.assetBuyer,
buyQuote: state.latestBuyQuote,
affiliateInfo: state.affiliateInfo,
onViewTransaction: () => {
if (
state.assetBuyer &&

View File

@@ -6,12 +6,13 @@ import * as _ from 'lodash';
import * as React from 'react';
import { connect } from 'react-redux';
import { Dispatch } from 'redux';
import { oc } from 'ts-optchain';
import { ERC20AssetAmountInput } from '../components/erc20_asset_amount_input';
import { Action, actions } from '../redux/actions';
import { State } from '../redux/reducer';
import { ColorOption } from '../style/theme';
import { ERC20Asset, OrderProcessState } from '../types';
import { AffiliateInfo, ERC20Asset, OrderProcessState } from '../types';
import { assetUtils } from '../util/asset';
import { BigNumberInput } from '../util/big_number_input';
import { errorFlasher } from '../util/error_flasher';
@@ -27,10 +28,16 @@ interface ConnectedState {
value?: BigNumberInput;
asset?: ERC20Asset;
isDisabled: boolean;
affiliateInfo?: AffiliateInfo;
}
interface ConnectedDispatch {
updateBuyQuote: (assetBuyer?: AssetBuyer, value?: BigNumberInput, asset?: ERC20Asset) => void;
updateBuyQuote: (
assetBuyer?: AssetBuyer,
value?: BigNumberInput,
asset?: ERC20Asset,
affiliateInfo?: AffiliateInfo,
) => void;
}
interface ConnectedProps {
@@ -60,6 +67,7 @@ const mapStateToProps = (state: State, _ownProps: SelectedERC20AssetAmountInputP
value: state.selectedAssetAmount,
asset: selectedAsset as ERC20Asset,
isDisabled,
affiliateInfo: state.affiliateInfo,
};
};
@@ -68,6 +76,7 @@ const updateBuyQuoteAsync = async (
dispatch: Dispatch<Action>,
asset: ERC20Asset,
assetAmount: BigNumber,
affiliateInfo?: AffiliateInfo,
): Promise<void> => {
// get a new buy quote.
const baseUnitValue = Web3Wrapper.toBaseUnitAmount(assetAmount, asset.metaData.decimals);
@@ -75,9 +84,10 @@ const updateBuyQuoteAsync = async (
// mark quote as pending
dispatch(actions.setQuoteRequestStatePending());
const feePercentage = oc(affiliateInfo).feePercentage();
let newBuyQuote: BuyQuote | undefined;
try {
newBuyQuote = await assetBuyer.getBuyQuoteAsync(asset.assetData, baseUnitValue);
newBuyQuote = await assetBuyer.getBuyQuoteAsync(asset.assetData, baseUnitValue, { feePercentage });
} catch (error) {
dispatch(actions.setQuoteRequestStateFailure());
let errorMessage;
@@ -93,7 +103,11 @@ const updateBuyQuoteAsync = async (
const assetName = assetUtils.bestNameForAsset(asset, 'This asset');
errorMessage = `${assetName} is currently unavailable`;
}
errorFlasher.flashNewErrorMessage(dispatch, errorMessage);
if (!_.isUndefined(errorMessage)) {
errorFlasher.flashNewErrorMessage(dispatch, errorMessage);
} else {
throw error;
}
return;
}
// We have a successful new buy quote
@@ -108,7 +122,7 @@ const mapDispatchToProps = (
dispatch: Dispatch<Action>,
_ownProps: SelectedERC20AssetAmountInputProps,
): ConnectedDispatch => ({
updateBuyQuote: (assetBuyer, value, asset) => {
updateBuyQuote: (assetBuyer, value, asset, affiliateInfo) => {
// Update the input
dispatch(actions.updateSelectedAssetAmount(value));
// invalidate the last buy quote.
@@ -120,7 +134,7 @@ const mapDispatchToProps = (
// even if it's debounced, give them the illusion it's loading
dispatch(actions.setQuoteRequestStatePending());
// tslint:disable-next-line:no-floating-promises
debouncedUpdateBuyQuoteAsync(assetBuyer, dispatch, asset, value);
debouncedUpdateBuyQuoteAsync(assetBuyer, dispatch, asset, value, affiliateInfo);
}
},
});
@@ -135,7 +149,7 @@ const mergeProps = (
asset: connectedState.asset,
value: connectedState.value,
onChange: (value, asset) => {
connectedDispatch.updateBuyQuote(connectedState.assetBuyer, value, asset);
connectedDispatch.updateBuyQuote(connectedState.assetBuyer, value, asset, connectedState.affiliateInfo);
},
isDisabled: connectedState.isDisabled,
};

View File

@@ -24,6 +24,9 @@ export const render = (props: ZeroExInstantOverlayProps, selector: string = DEFA
if (!_.isUndefined(props.zIndex)) {
assert.isNumber('props.zIndex', props.zIndex);
}
if (!_.isUndefined(props.affiliateInfo)) {
assert.isValidaffiliateInfo('props.affiliateInfo', props.affiliateInfo);
}
const appendToIfExists = document.querySelector(selector);
assert.assert(!_.isNull(appendToIfExists), `Could not find div with selector: ${selector}`);
const appendTo = appendToIfExists as Element;

View File

@@ -6,6 +6,7 @@ import * as _ from 'lodash';
import { assetMetaDataMap } from '../data/asset_meta_data_map';
import {
AffiliateInfo,
Asset,
AssetMetaData,
AsyncProcessState,
@@ -31,6 +32,7 @@ export interface State {
quoteRequestState: AsyncProcessState;
latestErrorMessage?: string;
latestErrorDisplayStatus: DisplayStatus;
affiliateInfo?: AffiliateInfo;
}
export const INITIAL_STATE: State = {
@@ -43,6 +45,7 @@ export const INITIAL_STATE: State = {
latestErrorMessage: undefined,
latestErrorDisplayStatus: DisplayStatus.Hidden,
quoteRequestState: AsyncProcessState.NONE,
affiliateInfo: undefined,
};
export const reducer = (state: State = INITIAL_STATE, action: Action): State => {

View File

@@ -80,3 +80,8 @@ export enum ZeroExInstantError {
AssetMetaDataNotAvailable = 'ASSET_META_DATA_NOT_AVAILABLE',
InsufficientETH = 'INSUFFICIENT_ETH',
}
export interface AffiliateInfo {
feeRecipient: string;
feePercentage: number;
}

View File

@@ -4,7 +4,7 @@ import { assetDataUtils } from '@0x/order-utils';
import { AssetProxyId, ObjectMap, SignedOrder } from '@0x/types';
import * as _ from 'lodash';
import { AssetMetaData } from '../types';
import { AffiliateInfo, AssetMetaData } from '../types';
export const assert = {
...sharedAssert,
@@ -41,4 +41,12 @@ export const assert = {
assert.isUri(`${variableName}.imageUrl`, metaData.imageUrl);
}
},
isValidaffiliateInfo(variableName: string, affiliateInfo: AffiliateInfo): void {
assert.isETHAddressHex(`${variableName}.recipientAddress`, affiliateInfo.feeRecipient);
assert.isNumber(`${variableName}.percentage`, affiliateInfo.feePercentage);
assert.assert(
affiliateInfo.feePercentage >= 0 && affiliateInfo.feePercentage <= 0.05,
`Expected ${variableName}.percentage to be between 0 and 0.05, but is ${affiliateInfo.feePercentage}`,
);
},
};