diff options
author | Steve Klebanoff <steve.klebanoff@gmail.com> | 2019-01-03 07:17:26 +0800 |
---|---|---|
committer | Steve Klebanoff <steve.klebanoff@gmail.com> | 2019-01-03 07:17:26 +0800 |
commit | 65af195054e1f6de41d36a2d30d8342bef9752c0 (patch) | |
tree | 8559d71d6dd4269139afa5334b317f6efe90acd3 /packages/website/ts/pages/instant/config_generator.tsx | |
parent | 9f47f90c6e80ba9a61bcb6065fed1e2c6be8c5b7 (diff) | |
parent | 1ddf1087dd327b2ef35165518ee91eb457b84174 (diff) | |
download | dexon-sol-tools-65af195054e1f6de41d36a2d30d8342bef9752c0.tar dexon-sol-tools-65af195054e1f6de41d36a2d30d8342bef9752c0.tar.gz dexon-sol-tools-65af195054e1f6de41d36a2d30d8342bef9752c0.tar.bz2 dexon-sol-tools-65af195054e1f6de41d36a2d30d8342bef9752c0.tar.lz dexon-sol-tools-65af195054e1f6de41d36a2d30d8342bef9752c0.tar.xz dexon-sol-tools-65af195054e1f6de41d36a2d30d8342bef9752c0.tar.zst dexon-sol-tools-65af195054e1f6de41d36a2d30d8342bef9752c0.zip |
Merge branch 'development' into feature/monorepo/release-notes
Diffstat (limited to 'packages/website/ts/pages/instant/config_generator.tsx')
-rw-r--r-- | packages/website/ts/pages/instant/config_generator.tsx | 332 |
1 files changed, 332 insertions, 0 deletions
diff --git a/packages/website/ts/pages/instant/config_generator.tsx b/packages/website/ts/pages/instant/config_generator.tsx new file mode 100644 index 000000000..e43d47119 --- /dev/null +++ b/packages/website/ts/pages/instant/config_generator.tsx @@ -0,0 +1,332 @@ +import { StandardRelayerAPIOrderProvider } from '@0x/asset-buyer'; +import { getContractAddressesForNetworkOrThrow } from '@0x/contract-addresses'; +import { assetDataUtils } from '@0x/order-utils'; +import { ObjectMap } from '@0x/types'; +import * as _ from 'lodash'; +import * as React from 'react'; +import styled from 'styled-components'; + +import { CheckMark } from 'ts/components/ui/check_mark'; +import { Container } from 'ts/components/ui/container'; +import { MultiSelect } from 'ts/components/ui/multi_select'; +import { Spinner } from 'ts/components/ui/spinner'; +import { Text } from 'ts/components/ui/text'; +import { ConfigGeneratorAddressInput } from 'ts/pages/instant/config_generator_address_input'; +import { FeePercentageSlider } from 'ts/pages/instant/fee_percentage_slider'; +import { colors } from 'ts/style/colors'; +import { WebsitePaths } from 'ts/types'; +import { constants } from 'ts/utils/constants'; + +// New components +import { Heading } from 'ts/components/text'; +import { Select, SelectItemConfig } from 'ts/pages/instant/select'; + +import { assetMetaDataMap } from '../../../../instant/src/data/asset_meta_data_map'; +import { ERC20AssetMetaData, ZeroExInstantBaseConfig } from '../../../../instant/src/types'; + +export interface ConfigGeneratorProps { + value: ZeroExInstantBaseConfig; + onConfigChange: (config: ZeroExInstantBaseConfig) => void; +} + +export interface ConfigGeneratorState { + isLoadingAvailableTokens: boolean; + // Address to token info + availableTokens?: ObjectMap<ERC20AssetMetaData>; +} + +const SRA_ENDPOINTS = ['https://api.radarrelay.com/0x/v2/', 'https://sra.bamboorelay.com/0x/v2/']; + +export class ConfigGenerator extends React.Component<ConfigGeneratorProps, ConfigGeneratorState> { + public state: ConfigGeneratorState = { + isLoadingAvailableTokens: true, + }; + public componentDidMount(): void { + // tslint:disable-next-line:no-floating-promises + this._setAvailableAssetsFromOrderProvider(); + } + public componentDidUpdate(prevProps: ConfigGeneratorProps): void { + if (prevProps.value.orderSource !== this.props.value.orderSource) { + // tslint:disable-next-line:no-floating-promises + this._setAvailableAssetsFromOrderProvider(); + const newConfig: ZeroExInstantBaseConfig = { + ...this.props.value, + availableAssetDatas: undefined, + }; + this.props.onConfigChange(newConfig); + } + } + public render(): React.ReactNode { + const { value } = this.props; + if (!_.isString(value.orderSource)) { + throw new Error('ConfigGenerator component only supports string values as an orderSource.'); + } + return ( + <Container minWidth="350px"> + <ConfigGeneratorSection title="Liquidity Source"> + <Select + shouldIncludeEmpty={false} + id="" + value={value.orderSource} + items={this._generateItems()} + onChange={this._handleSRASelection.bind(this)} + /> + </ConfigGeneratorSection> + <ConfigGeneratorSection {...this._getTokenSelectorProps()}> + {this._renderTokenMultiSelectOrSpinner()} + </ConfigGeneratorSection> + <ConfigGeneratorSection title="Transaction fee ETH address" marginBottom="10px" isOptional={true}> + <ConfigGeneratorAddressInput + value={value.affiliateInfo ? value.affiliateInfo.feeRecipient : ''} + onChange={this._handleAffiliateAddressChange} + /> + </ConfigGeneratorSection> + <ConfigGeneratorSection + title="Fee percentage" + actionText="Learn more" + onActionTextClick={this._handleAffiliatePercentageLearnMoreClick} + > + <FeePercentageSlider + value={value.affiliateInfo.feePercentage} + onChange={this._handleAffiliatePercentageChange} + isDisabled={ + _.isUndefined(value.affiliateInfo) || + _.isUndefined(value.affiliateInfo.feeRecipient) || + _.isEmpty(value.affiliateInfo.feeRecipient) + } + /> + </ConfigGeneratorSection> + </Container> + ); + } + private readonly _getTokenSelectorProps = (): ConfigGeneratorSectionProps => { + if (_.isEmpty(this.state.availableTokens)) { + return { + title: 'What tokens can users buy?', + }; + } + if (_.isUndefined(this.props.value.availableAssetDatas)) { + return { + title: 'What tokens can users buy?', + actionText: 'Unselect All', + onActionTextClick: this._handleUnselectAllClick, + }; + } + return { + title: 'What tokens can users buy?', + actionText: 'Select All', + onActionTextClick: this._handleSelectAllClick, + }; + }; + private readonly _generateItems = (): SelectItemConfig[] => { + return _.map(SRA_ENDPOINTS, endpoint => ({ + label: endpoint, + value: endpoint, + onClick: this._handleSRASelection.bind(this, endpoint), + })); + }; + private readonly _handleAffiliatePercentageLearnMoreClick = (): void => { + window.open(`${WebsitePaths.Wiki}#Learn-About-Affiliate-Fees`, '_blank'); + }; + private readonly _handleSRASelection = (event: React.ChangeEvent<HTMLSelectElement>) => { + const sraEndpoint = event.target.value; + const newConfig: ZeroExInstantBaseConfig = { + ...this.props.value, + orderSource: sraEndpoint, + }; + this.props.onConfigChange(newConfig); + }; + private readonly _handleAffiliateAddressChange = (address: string, isValid: boolean) => { + const oldConfig: ZeroExInstantBaseConfig = this.props.value; + const newConfig: ZeroExInstantBaseConfig = { + ...oldConfig, + affiliateInfo: { + feeRecipient: address, + feePercentage: oldConfig.affiliateInfo.feePercentage, + }, + }; + this.props.onConfigChange(newConfig); + }; + private readonly _handleAffiliatePercentageChange = (value: number) => { + const oldConfig: ZeroExInstantBaseConfig = this.props.value; + const newConfig: ZeroExInstantBaseConfig = { + ...oldConfig, + affiliateInfo: { + feeRecipient: oldConfig.affiliateInfo.feeRecipient, + feePercentage: value, + }, + }; + this.props.onConfigChange(newConfig); + }; + private readonly _handleSelectAllClick = () => { + const newConfig: ZeroExInstantBaseConfig = { + ...this.props.value, + availableAssetDatas: undefined, + }; + this.props.onConfigChange(newConfig); + }; + private readonly _handleUnselectAllClick = () => { + const newConfig: ZeroExInstantBaseConfig = { + ...this.props.value, + availableAssetDatas: [], + }; + this.props.onConfigChange(newConfig); + }; + private readonly _handleTokenClick = (assetData: string) => { + const { value } = this.props; + let newAvailableAssetDatas: string[] = []; + const allKnownAssetDatas = _.keys(this.state.availableTokens); + const availableAssetDatas = value.availableAssetDatas; + if (_.isUndefined(availableAssetDatas)) { + // It being undefined means it's all tokens. + newAvailableAssetDatas = _.pull(allKnownAssetDatas, assetData); + } else if (!_.includes(availableAssetDatas, assetData)) { + // Add it + newAvailableAssetDatas = [...availableAssetDatas, assetData]; + if (newAvailableAssetDatas.length === allKnownAssetDatas.length) { + // If all tokens are manually selected, just show none. + newAvailableAssetDatas = undefined; + } + } else { + // Remove it + newAvailableAssetDatas = _.pull(availableAssetDatas, assetData); + } + const newConfig: ZeroExInstantBaseConfig = { + ...this.props.value, + availableAssetDatas: newAvailableAssetDatas, + }; + this.props.onConfigChange(newConfig); + }; + private readonly _setAvailableAssetsFromOrderProvider = async (): Promise<void> => { + const { value } = this.props; + if (!_.isUndefined(value.orderSource) && _.isString(value.orderSource)) { + this.setState({ isLoadingAvailableTokens: true }); + const networkId = constants.NETWORK_ID_MAINNET; + const sraOrderProvider = new StandardRelayerAPIOrderProvider(value.orderSource, networkId); + const etherTokenAddress = getContractAddressesForNetworkOrThrow(networkId).etherToken; + const etherTokenAssetData = assetDataUtils.encodeERC20AssetData(etherTokenAddress); + const assetDatas = await sraOrderProvider.getAvailableMakerAssetDatasAsync(etherTokenAssetData); + const availableTokens = _.reduce( + assetDatas, + (acc, assetData) => { + const metaDataIfExists = assetMetaDataMap[assetData] as ERC20AssetMetaData; + if (metaDataIfExists) { + acc[assetData] = metaDataIfExists; + } + return acc; + }, + {} as ObjectMap<ERC20AssetMetaData>, + ); + this.setState({ availableTokens, isLoadingAvailableTokens: false }); + } + }; + private readonly _renderTokenMultiSelectOrSpinner = (): React.ReactNode => { + const { value } = this.props; + const { availableTokens, isLoadingAvailableTokens } = this.state; + const multiSelectHeight = '200px'; + if (isLoadingAvailableTokens) { + return ( + <Container + className="flex flex-column items-center justify-center" + height={multiSelectHeight} + backgroundColor={colors.white} + borderRadius="4px" + width="100%" + > + <Container position="relative" left="12px" marginBottom="20px"> + <Spinner /> + </Container> + <Text fontSize="16px">Loading...</Text> + </Container> + ); + } + const availableAssetDatas = _.keys(availableTokens); + if (availableAssetDatas.length === 0) { + return ( + <Container + className="flex flex-column items-center justify-center" + height={multiSelectHeight} + backgroundColor={colors.white} + borderRadius="4px" + width="100%" + > + <Text fontSize="16px">No tokens available. Try another endpoint?</Text> + </Container> + ); + } + const items = _.map(_.keys(availableTokens), assetData => { + const metaData = availableTokens[assetData]; + return { + value: assetData, + renderItemContent: (isSelected: boolean) => ( + <Container className="flex items-center"> + <Container marginRight="10px"> + <CheckMark isChecked={isSelected} color={colors.brandLight} /> + </Container> + <CheckboxText isSelected={isSelected}> + {metaData.symbol.toUpperCase()} — {metaData.name} + </CheckboxText> + </Container> + ), + onClick: this._handleTokenClick.bind(this, assetData), + }; + }); + return <MultiSelect items={items} selectedValues={value.availableAssetDatas} height={multiSelectHeight} />; + }; +} + +export interface ConfigGeneratorSectionProps { + title: string; + actionText?: string; + onActionTextClick?: () => void; + isOptional?: boolean; + marginBottom?: string; +} + +export const ConfigGeneratorSection: React.StatelessComponent<ConfigGeneratorSectionProps> = ({ + title, + actionText, + onActionTextClick, + isOptional, + marginBottom, + children, +}) => ( + <Container marginBottom={marginBottom}> + <Container marginBottom="10px" className="flex justify-between items-center"> + <Heading size="small" marginBottom="0" isFlex={true}> + <span>{title}</span> + {isOptional && <OptionalText> Optional</OptionalText>} + </Heading> + {actionText && <OptionalAction onClick={onActionTextClick}>{actionText}</OptionalAction>} + </Container> + {children} + </Container> +); + +ConfigGeneratorSection.defaultProps = { + marginBottom: '30px', +}; + +const OptionalText = styled.span` + display: inline; + font-size: 14px; + color: #999999; + flex-shrink: 0; +`; + +interface CheckboxTextProps { + isSelected?: boolean; +} + +const CheckboxText = + styled.span < + CheckboxTextProps > + ` + font-size: 14px; + line-height: 18px; + color: ${props => (props.isSelected ? colors.brandDark : '#666666')} +`; + +const OptionalAction = styled(OptionalText)` + cursor: pointer; +`; |