diff options
Diffstat (limited to 'packages/instant/src/containers')
8 files changed, 443 insertions, 0 deletions
diff --git a/packages/instant/src/containers/available_erc20_token_selector.ts b/packages/instant/src/containers/available_erc20_token_selector.ts new file mode 100644 index 000000000..4d4218d22 --- /dev/null +++ b/packages/instant/src/containers/available_erc20_token_selector.ts @@ -0,0 +1,45 @@ +import * as _ from 'lodash'; +import * as React from 'react'; +import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; + +import { State } from '../redux/reducer'; +import { ERC20Asset } from '../types'; +import { assetUtils } from '../util/asset'; + +import { ERC20TokenSelector } from '../components/erc20_token_selector'; +import { Action, actions } from '../redux/actions'; + +export interface AvailableERC20TokenSelectorProps { + onTokenSelect?: (token: ERC20Asset) => void; +} + +interface ConnectedState { + tokens: ERC20Asset[]; +} + +interface ConnectedDispatch { + onTokenSelect: (token: ERC20Asset) => void; +} + +const mapStateToProps = (state: State, _ownProps: AvailableERC20TokenSelectorProps): ConnectedState => ({ + tokens: assetUtils.getERC20AssetsFromAssets(state.availableAssets || []), +}); + +const mapDispatchToProps = ( + dispatch: Dispatch<Action>, + ownProps: AvailableERC20TokenSelectorProps, +): ConnectedDispatch => ({ + onTokenSelect: (token: ERC20Asset) => { + dispatch(actions.updateSelectedAsset(token)); + dispatch(actions.resetAmount()); + if (ownProps.onTokenSelect) { + ownProps.onTokenSelect(token); + } + }, +}); + +export const AvailableERC20TokenSelector: React.ComponentClass<AvailableERC20TokenSelectorProps> = connect( + mapStateToProps, + mapDispatchToProps, +)(ERC20TokenSelector); diff --git a/packages/instant/src/containers/latest_buy_quote_order_details.ts b/packages/instant/src/containers/latest_buy_quote_order_details.ts new file mode 100644 index 000000000..2b59ed3ae --- /dev/null +++ b/packages/instant/src/containers/latest_buy_quote_order_details.ts @@ -0,0 +1,30 @@ +import { BuyQuoteInfo } from '@0x/asset-buyer'; +import { BigNumber } from '@0x/utils'; +import * as _ from 'lodash'; +import * as React from 'react'; +import { connect } from 'react-redux'; +import { oc } from 'ts-optchain'; + +import { State } from '../redux/reducer'; + +import { OrderDetails } from '../components/order_details'; +import { AsyncProcessState } from '../types'; + +export interface LatestBuyQuoteOrderDetailsProps {} + +interface ConnectedState { + buyQuoteInfo?: BuyQuoteInfo; + ethUsdPrice?: BigNumber; + isLoading: boolean; +} + +const mapStateToProps = (state: State, _ownProps: LatestBuyQuoteOrderDetailsProps): ConnectedState => ({ + // use the worst case quote info + buyQuoteInfo: oc(state).latestBuyQuote.worstCaseQuoteInfo(), + ethUsdPrice: state.ethUsdPrice, + isLoading: state.quoteRequestState === AsyncProcessState.Pending, +}); + +export const LatestBuyQuoteOrderDetails: React.ComponentClass<LatestBuyQuoteOrderDetailsProps> = connect( + mapStateToProps, +)(OrderDetails); diff --git a/packages/instant/src/containers/latest_error.tsx b/packages/instant/src/containers/latest_error.tsx new file mode 100644 index 000000000..99e55a6c4 --- /dev/null +++ b/packages/instant/src/containers/latest_error.tsx @@ -0,0 +1,35 @@ +import * as React from 'react'; + +import { connect } from 'react-redux'; + +import { SlideAnimationState } from '../components/animations/slide_animation'; +import { SlidingError } from '../components/sliding_error'; +import { State } from '../redux/reducer'; +import { Asset, DisplayStatus } from '../types'; + +export interface LatestErrorComponentProps { + asset?: Asset; + latestErrorMessage?: string; + animationState: SlideAnimationState; +} + +export const LatestErrorComponent: React.StatelessComponent<LatestErrorComponentProps> = props => { + if (!props.latestErrorMessage) { + return <div />; + } + return <SlidingError animationState={props.animationState} icon="😢" message={props.latestErrorMessage} />; +}; + +interface ConnectedState { + asset?: Asset; + latestErrorMessage?: string; + animationState: SlideAnimationState; +} +export interface LatestErrorProps {} +const mapStateToProps = (state: State, _ownProps: LatestErrorProps): ConnectedState => ({ + asset: state.selectedAsset, + latestErrorMessage: state.latestErrorMessage, + animationState: state.latestErrorDisplayStatus === DisplayStatus.Present ? 'slidIn' : 'slidOut', +}); + +export const LatestError = connect(mapStateToProps)(LatestErrorComponent); diff --git a/packages/instant/src/containers/selected_asset_buy_order_progress.ts b/packages/instant/src/containers/selected_asset_buy_order_progress.ts new file mode 100644 index 000000000..7c8c24676 --- /dev/null +++ b/packages/instant/src/containers/selected_asset_buy_order_progress.ts @@ -0,0 +1,13 @@ +import { connect } from 'react-redux'; + +import { BuyOrderProgress } from '../components/buy_order_progress'; +import { State } from '../redux/reducer'; +import { OrderState } from '../types'; + +interface ConnectedState { + buyOrderState: OrderState; +} +const mapStateToProps = (state: State, _ownProps: {}): ConnectedState => ({ + buyOrderState: state.buyOrderState, +}); +export const SelectedAssetBuyOrderProgress = connect(mapStateToProps)(BuyOrderProgress); diff --git a/packages/instant/src/containers/selected_asset_buy_order_state_buttons.ts b/packages/instant/src/containers/selected_asset_buy_order_state_buttons.ts new file mode 100644 index 000000000..c3a5e88b9 --- /dev/null +++ b/packages/instant/src/containers/selected_asset_buy_order_state_buttons.ts @@ -0,0 +1,92 @@ +import { AssetBuyer, AssetBuyerError, BuyQuote } from '@0x/asset-buyer'; +import * as _ from 'lodash'; +import * as React from 'react'; +import { connect } from 'react-redux'; +import { Dispatch } from 'redux'; + +import { BuyOrderStateButtons } from '../components/buy_order_state_buttons'; +import { Action, actions } from '../redux/actions'; +import { State } from '../redux/reducer'; +import { AffiliateInfo, OrderProcessState, ZeroExInstantError } from '../types'; +import { errorFlasher } from '../util/error_flasher'; +import { etherscanUtil } from '../util/etherscan'; + +interface ConnectedState { + buyQuote?: BuyQuote; + buyOrderProcessingState: OrderProcessState; + assetBuyer: AssetBuyer; + affiliateInfo?: AffiliateInfo; + onViewTransaction: () => void; +} + +interface ConnectedDispatch { + onValidationPending: (buyQuote: BuyQuote) => void; + onSignatureDenied: (buyQuote: BuyQuote) => void; + onBuyProcessing: (buyQuote: BuyQuote, txHash: string, startTimeUnix: number, expectedEndTimeUnix: number) => void; + onBuySuccess: (buyQuote: BuyQuote, txHash: string) => void; + onBuyFailure: (buyQuote: BuyQuote, txHash: string) => void; + onRetry: () => void; + onValidationFail: (buyQuote: BuyQuote, errorMessage: AssetBuyerError | ZeroExInstantError) => void; +} +export interface SelectedAssetBuyOrderStateButtons {} +const mapStateToProps = (state: State, _ownProps: SelectedAssetBuyOrderStateButtons): ConnectedState => { + const assetBuyer = state.providerState.assetBuyer; + return { + buyOrderProcessingState: state.buyOrderState.processState, + assetBuyer, + buyQuote: state.latestBuyQuote, + affiliateInfo: state.affiliateInfo, + onViewTransaction: () => { + if ( + state.buyOrderState.processState === OrderProcessState.Processing || + state.buyOrderState.processState === OrderProcessState.Success || + state.buyOrderState.processState === OrderProcessState.Failure + ) { + const etherscanUrl = etherscanUtil.getEtherScanTxnAddressIfExists( + state.buyOrderState.txHash, + assetBuyer.networkId, + ); + if (etherscanUrl) { + window.open(etherscanUrl, '_blank'); + return; + } + } + }, + }; +}; + +const mapDispatchToProps = ( + dispatch: Dispatch<Action>, + ownProps: SelectedAssetBuyOrderStateButtons, +): ConnectedDispatch => ({ + onValidationPending: (buyQuote: BuyQuote) => { + dispatch(actions.setBuyOrderStateValidating()); + }, + onBuyProcessing: (buyQuote: BuyQuote, txHash: string, startTimeUnix: number, expectedEndTimeUnix: number) => { + dispatch(actions.setBuyOrderStateProcessing(txHash, startTimeUnix, expectedEndTimeUnix)); + }, + onBuySuccess: (buyQuote: BuyQuote, txHash: string) => dispatch(actions.setBuyOrderStateSuccess(txHash)), + onBuyFailure: (buyQuote: BuyQuote, txHash: string) => dispatch(actions.setBuyOrderStateFailure(txHash)), + onSignatureDenied: () => { + dispatch(actions.resetAmount()); + const errorMessage = 'You denied this transaction'; + errorFlasher.flashNewErrorMessage(dispatch, errorMessage); + }, + onValidationFail: (buyQuote, error) => { + dispatch(actions.setBuyOrderStateNone()); + if (error === ZeroExInstantError.InsufficientETH) { + const errorMessage = "You don't have enough ETH"; + errorFlasher.flashNewErrorMessage(dispatch, errorMessage); + } else { + errorFlasher.flashNewErrorMessage(dispatch); + } + }, + onRetry: () => { + dispatch(actions.resetAmount()); + }, +}); + +export const SelectedAssetBuyOrderStateButtons: React.ComponentClass<SelectedAssetBuyOrderStateButtons> = connect( + mapStateToProps, + mapDispatchToProps, +)(BuyOrderStateButtons); diff --git a/packages/instant/src/containers/selected_asset_instant_heading.ts b/packages/instant/src/containers/selected_asset_instant_heading.ts new file mode 100644 index 000000000..a407279e6 --- /dev/null +++ b/packages/instant/src/containers/selected_asset_instant_heading.ts @@ -0,0 +1,34 @@ +import { BigNumber } from '@0x/utils'; +import * as _ from 'lodash'; +import * as React from 'react'; +import { connect } from 'react-redux'; +import { oc } from 'ts-optchain'; + +import { State } from '../redux/reducer'; +import { AsyncProcessState, ERC20Asset, OrderState } from '../types'; + +import { InstantHeading } from '../components/instant_heading'; + +export interface InstantHeadingProps { + onSelectAssetClick?: (asset?: ERC20Asset) => void; +} + +interface ConnectedState { + selectedAssetAmount?: BigNumber; + totalEthBaseAmount?: BigNumber; + ethUsdPrice?: BigNumber; + quoteRequestState: AsyncProcessState; + buyOrderState: OrderState; +} + +const mapStateToProps = (state: State, _ownProps: InstantHeadingProps): ConnectedState => ({ + selectedAssetAmount: state.selectedAssetAmount, + totalEthBaseAmount: oc(state).latestBuyQuote.worstCaseQuoteInfo.totalEthAmount(), + ethUsdPrice: state.ethUsdPrice, + quoteRequestState: state.quoteRequestState, + buyOrderState: state.buyOrderState, +}); + +export const SelectedAssetInstantHeading: React.ComponentClass<InstantHeadingProps> = connect(mapStateToProps)( + InstantHeading, +); diff --git a/packages/instant/src/containers/selected_asset_theme_provider.ts b/packages/instant/src/containers/selected_asset_theme_provider.ts new file mode 100644 index 000000000..6e6b83d73 --- /dev/null +++ b/packages/instant/src/containers/selected_asset_theme_provider.ts @@ -0,0 +1,32 @@ +import * as _ from 'lodash'; +import * as React from 'react'; +import { connect } from 'react-redux'; + +import { State } from '../redux/reducer'; +import { Theme, theme as defaultTheme, ThemeProvider } from '../style/theme'; +import { Asset } from '../types'; + +export interface SelectedAssetThemeProviderProps {} + +interface ConnectedState { + theme: Theme; +} + +const getTheme = (asset?: Asset): Theme => { + if (!_.isUndefined(asset) && !_.isUndefined(asset.metaData.primaryColor)) { + return { + ...defaultTheme, + primaryColor: asset.metaData.primaryColor, + }; + } + return defaultTheme; +}; + +const mapStateToProps = (state: State, _ownProps: SelectedAssetThemeProviderProps): ConnectedState => { + const theme = getTheme(state.selectedAsset); + return { theme }; +}; + +export const SelectedAssetThemeProvider: React.ComponentClass<SelectedAssetThemeProviderProps> = connect( + mapStateToProps, +)(ThemeProvider); diff --git a/packages/instant/src/containers/selected_erc20_asset_amount_input.ts b/packages/instant/src/containers/selected_erc20_asset_amount_input.ts new file mode 100644 index 000000000..784eb4bd0 --- /dev/null +++ b/packages/instant/src/containers/selected_erc20_asset_amount_input.ts @@ -0,0 +1,162 @@ +import { AssetBuyer, AssetBuyerError, BuyQuote } from '@0x/asset-buyer'; +import { AssetProxyId } from '@0x/types'; +import { BigNumber } from '@0x/utils'; +import { Web3Wrapper } from '@0x/web3-wrapper'; +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 { AffiliateInfo, ERC20Asset, OrderProcessState } from '../types'; +import { assetUtils } from '../util/asset'; +import { errorFlasher } from '../util/error_flasher'; + +export interface SelectedERC20AssetAmountInputProps { + fontColor?: ColorOption; + startingFontSizePx: number; + onSelectAssetClick?: (asset?: ERC20Asset) => void; +} + +interface ConnectedState { + assetBuyer: AssetBuyer; + value?: BigNumber; + asset?: ERC20Asset; + isDisabled: boolean; + numberOfAssetsAvailable?: number; + affiliateInfo?: AffiliateInfo; +} + +interface ConnectedDispatch { + updateBuyQuote: ( + assetBuyer: AssetBuyer, + value?: BigNumber, + asset?: ERC20Asset, + affiliateInfo?: AffiliateInfo, + ) => void; +} + +interface ConnectedProps { + value?: BigNumber; + asset?: ERC20Asset; + onChange: (value?: BigNumber, asset?: ERC20Asset) => void; + isDisabled: boolean; + numberOfAssetsAvailable?: number; +} + +type FinalProps = ConnectedProps & SelectedERC20AssetAmountInputProps; + +const mapStateToProps = (state: State, _ownProps: SelectedERC20AssetAmountInputProps): ConnectedState => { + const processState = state.buyOrderState.processState; + const isEnabled = processState === OrderProcessState.None || processState === OrderProcessState.Failure; + const isDisabled = !isEnabled; + const selectedAsset = + !_.isUndefined(state.selectedAsset) && state.selectedAsset.metaData.assetProxyId === AssetProxyId.ERC20 + ? (state.selectedAsset as ERC20Asset) + : undefined; + const numberOfAssetsAvailable = _.isUndefined(state.availableAssets) ? undefined : state.availableAssets.length; + const assetBuyer = state.providerState.assetBuyer; + return { + assetBuyer, + value: state.selectedAssetAmount, + asset: selectedAsset, + isDisabled, + numberOfAssetsAvailable, + affiliateInfo: state.affiliateInfo, + }; +}; + +const updateBuyQuoteAsync = async ( + assetBuyer: AssetBuyer, + dispatch: Dispatch<Action>, + asset: ERC20Asset, + assetAmount: BigNumber, + affiliateInfo?: AffiliateInfo, +): Promise<void> => { + // get a new buy quote. + const baseUnitValue = Web3Wrapper.toBaseUnitAmount(assetAmount, asset.metaData.decimals); + + // mark quote as pending + dispatch(actions.setQuoteRequestStatePending()); + + const feePercentage = oc(affiliateInfo).feePercentage(); + let newBuyQuote: BuyQuote | undefined; + try { + newBuyQuote = await assetBuyer.getBuyQuoteAsync(asset.assetData, baseUnitValue, { feePercentage }); + } catch (error) { + dispatch(actions.setQuoteRequestStateFailure()); + let errorMessage; + if (error.message === AssetBuyerError.InsufficientAssetLiquidity) { + const assetName = assetUtils.bestNameForAsset(asset, 'of this asset'); + errorMessage = `Not enough ${assetName} available`; + } else if (error.message === AssetBuyerError.InsufficientZrxLiquidity) { + errorMessage = 'Not enough ZRX available'; + } else if ( + error.message === AssetBuyerError.StandardRelayerApiError || + error.message.startsWith(AssetBuyerError.AssetUnavailable) + ) { + const assetName = assetUtils.bestNameForAsset(asset, 'This asset'); + errorMessage = `${assetName} is currently unavailable`; + } + if (!_.isUndefined(errorMessage)) { + errorFlasher.flashNewErrorMessage(dispatch, errorMessage); + } else { + throw error; + } + return; + } + // We have a successful new buy quote + errorFlasher.clearError(dispatch); + // invalidate the last buy quote. + dispatch(actions.updateLatestBuyQuote(newBuyQuote)); +}; + +const debouncedUpdateBuyQuoteAsync = _.debounce(updateBuyQuoteAsync, 200, { trailing: true }); + +const mapDispatchToProps = ( + dispatch: Dispatch<Action>, + _ownProps: SelectedERC20AssetAmountInputProps, +): ConnectedDispatch => ({ + updateBuyQuote: (assetBuyer, value, asset, affiliateInfo) => { + // Update the input + dispatch(actions.updateSelectedAssetAmount(value)); + // invalidate the last buy quote. + dispatch(actions.updateLatestBuyQuote(undefined)); + // reset our buy state + dispatch(actions.setBuyOrderStateNone()); + + if (!_.isUndefined(value) && value.greaterThan(0) && !_.isUndefined(asset)) { + // 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, affiliateInfo); + } + }, +}); + +const mergeProps = ( + connectedState: ConnectedState, + connectedDispatch: ConnectedDispatch, + ownProps: SelectedERC20AssetAmountInputProps, +): FinalProps => { + return { + ...ownProps, + asset: connectedState.asset, + value: connectedState.value, + onChange: (value, asset) => { + connectedDispatch.updateBuyQuote(connectedState.assetBuyer, value, asset, connectedState.affiliateInfo); + }, + isDisabled: connectedState.isDisabled, + numberOfAssetsAvailable: connectedState.numberOfAssetsAvailable, + }; +}; + +export const SelectedERC20AssetAmountInput: React.ComponentClass<SelectedERC20AssetAmountInputProps> = connect( + mapStateToProps, + mapDispatchToProps, + mergeProps, +)(ERC20AssetAmountInput); |