From b939e8cedac349e627ea9f5dc0fcea594e3dfa71 Mon Sep 17 00:00:00 2001 From: Danny Skubak Date: Tue, 2 Jan 2018 22:18:10 -0500 Subject: [PATCH] Request Payment SubTab - EIP 681 (#671) * progress * Normalize bity api response * Filter api response * Track swap information in component state * Update dropdown onchange * remove dead code * Update Min Max Validation * Update minmax err msg && fix onChangeOriginKind * Add origin & destination to redux state * Update types & Update tests * Update types * Update swap.spec.ts test * Remove commented out code * Remove hardcoded coin array * Create types.ts for swap reducer * Update swapinput type * Update bityRates in localStorage & Replace all instances of ...Kind / ...Amount props * Add shapeshift banner * initial work for sagas * Update Types * Update swap reducer initial state * Update Types & Store empty obj for bityRates / options * Update more types * added shapeshift file and rates comments * action reducers and prop mapping to components * add typings and swap icon * more actions reducers and sagas * debugging shapeshift service * add Headers * Fix content type * add order reset saga and ui fixes * remove console log and swap b/w Bity and Shapeshift * working state for Shapeshift and Bity - tested with mainnet * add icon component * UI improvements and fix select bug * fix timer bug * add bity fallback options and toFixed floats * tslint errors * add arrow to dropdown and add support footer * Add service provider * fix minor $ bug and stop timer on order complete * better load UX and dropdown UX * fixed single test * currRate prop bugs and reduce LS bloat * takeEvery on timer saga and don't clear state.options to restartSwap reducer * export tx sagas and fix minor type * Add ShapeShift Rates functionality when selecting a ShapeShift pair. * type fixes * BugFix: Don't change displayed ShapeShift Rate Inputs on every dropdown change Also contains some caching / performance improvements * BugFix: Don't remote rate inputs when falsy amount * fix type error * Progress commit * Implement saga logic * Make address field factory component * Shorten debounce time * Make new actions / sagas for handling single token lookup * Implement working version of litesend * make unit dropdown searchable, add props for all tokens, custom validation * add string generators for EIP 681 token & ether transactions * add new selectors * add request payment tab * Change saga into selector * Add failing spec * fix broken test * add debounce to error message * fix tests * update snapshots * test coverage * move setState disabled property from debounce so we instantly can go to next step on valid amounts * reset amount value (useful for switching between tabs) * much deeper test coverage, fix debounce ux, and fix bity flashing at swap page load * fix minor failing test * seperate shapeshift erc20 token whitelist * fix saveState store bug * break orderTimeRemaining saga up and rewrite tests * fix tslint error * add isReadOnly prop to address field * use AddressField component, add additional validation * make prop optional * correct validation * allow for request tab to be used with view only wallet * account for undefined activeTab * add types --- common/components/AddressField.tsx | 8 +- common/components/AmountField.tsx | 18 +- .../components/UnitDropDown/UnitDropDown.tsx | 12 +- common/components/ui/Dropdown.tsx | 62 ++++-- .../components/RequestPayment.scss | 18 ++ .../components/RequestPayment.tsx | 186 ++++++++++++++++++ .../Tabs/SendTransaction/components/index.ts | 1 + .../containers/Tabs/SendTransaction/index.tsx | 10 +- .../containers/Tabs/SendTransaction/tabs.tsx | 14 +- common/libs/values.ts | 24 ++- common/sagas/swap/liteSend.ts | 1 - common/selectors/config.ts | 22 +++ 12 files changed, 347 insertions(+), 29 deletions(-) create mode 100644 common/containers/Tabs/SendTransaction/components/RequestPayment.scss create mode 100644 common/containers/Tabs/SendTransaction/components/RequestPayment.tsx diff --git a/common/components/AddressField.tsx b/common/components/AddressField.tsx index ad62a4b2..e70a401a 100644 --- a/common/components/AddressField.tsx +++ b/common/components/AddressField.tsx @@ -2,7 +2,11 @@ import React from 'react'; import { AddressFieldFactory } from './AddressFieldFactory'; import { donationAddressMap } from 'config/data'; -export const AddressField: React.SFC<{}> = () => ( +interface Props { + isReadOnly?: boolean; +} + +export const AddressField: React.SFC = ({ isReadOnly }) => ( ( = () => ( type="text" value={currentTo.raw} placeholder={donationAddressMap.ETH} - readOnly={!!readOnly} + readOnly={!!(isReadOnly || readOnly)} onChange={onChange} /> )} diff --git a/common/components/AmountField.tsx b/common/components/AmountField.tsx index aadff3df..aaaed0b0 100644 --- a/common/components/AmountField.tsx +++ b/common/components/AmountField.tsx @@ -6,26 +6,36 @@ import translate, { translateRaw } from 'translations'; interface Props { hasUnitDropdown?: boolean; + showAllTokens?: boolean; + customValidator?(rawAmount: string): boolean; } -export const AmountField: React.SFC = ({ hasUnitDropdown }) => ( +export const AmountField: React.SFC = ({ + hasUnitDropdown, + showAllTokens, + customValidator +}) => ( ( -
- {hasUnitDropdown && } + {hasUnitDropdown && }
)} /> ); + +const isAmountValid = (raw, customValidator, isValid) => + customValidator ? customValidator(raw) : isValid; diff --git a/common/components/UnitDropDown/UnitDropDown.tsx b/common/components/UnitDropDown/UnitDropDown.tsx index 8e4a82c8..1572545c 100644 --- a/common/components/UnitDropDown/UnitDropDown.tsx +++ b/common/components/UnitDropDown/UnitDropDown.tsx @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import { setUnitMeta, TSetUnitMeta } from 'actions/transaction'; import Dropdown from 'components/ui/Dropdown'; import { withConditional } from 'components/hocs'; -import { TokenBalance, getShownTokenBalances } from 'selectors/wallet'; +import { TokenBalance, MergedToken, getShownTokenBalances, getTokens } from 'selectors/wallet'; import { Query } from 'components/renderCbs'; import { connect } from 'react-redux'; import { AppState } from 'reducers'; @@ -15,6 +15,8 @@ interface DispatchProps { interface StateProps { unit: string; tokens: TokenBalance[]; + allTokens: MergedToken[]; + showAllTokens?: boolean; } const StringDropdown = Dropdown as new () => Dropdown; @@ -22,14 +24,15 @@ const ConditionalStringDropDown = withConditional(StringDropdown); class UnitDropdownClass extends Component { public render() { - const { tokens, unit } = this.props; + const { tokens, allTokens, showAllTokens, unit } = this.props; + const focusedTokens = showAllTokens ? allTokens : tokens; return (
( { this.props.setUnitMeta(unit); }; } -const getTokenSymbols = (tokens: TokenBalance[]) => tokens.map(t => t.symbol); +const getTokenSymbols = (tokens: (TokenBalance | MergedToken)[]) => tokens.map(t => t.symbol); function mapStateToProps(state: AppState) { return { tokens: getShownTokenBalances(state, true), + allTokens: getTokens(state), unit: getUnit(state) }; } diff --git a/common/components/ui/Dropdown.tsx b/common/components/ui/Dropdown.tsx index 68f31395..289a2488 100644 --- a/common/components/ui/Dropdown.tsx +++ b/common/components/ui/Dropdown.tsx @@ -15,7 +15,15 @@ interface Props { onChange?(value: T): void; } -export default class DropdownComponent extends Component, {}> { +interface State { + search: string; +} + +export default class DropdownComponent extends Component, State> { + public state = { + search: '' + }; + private dropdownShell: DropdownShell | null; public render() { @@ -45,25 +53,51 @@ export default class DropdownComponent extends Component, {}> { private renderOptions = () => { const { options, value, menuAlign, extra } = this.props; + const { search } = this.state; + const searchable = options.length > 20; const menuClass = classnames({ 'dropdown-menu': true, [`dropdown-menu-${menuAlign || ''}`]: !!menuAlign }); + const searchableStyle = { + maxHeight: '300px', + overflowY: 'auto' + }; + const searchRegex = new RegExp(search, 'gi'); + const onSearchChange = e => { + this.setState({ search: e.target.value }); + }; return ( -
    - {options.map((option, i) => { - return ( -
  • - - {this.props.formatTitle ? this.formatTitle(option) : option} - -
  • - ); - })} + diff --git a/common/containers/Tabs/SendTransaction/components/RequestPayment.scss b/common/containers/Tabs/SendTransaction/components/RequestPayment.scss new file mode 100644 index 00000000..ece27326 --- /dev/null +++ b/common/containers/Tabs/SendTransaction/components/RequestPayment.scss @@ -0,0 +1,18 @@ +@import "common/sass/variables"; + +.RequestPayment { + &-qr { + position: relative; + background: #FFF; + cursor: pointer; + } + + &-codeContainer { + padding-top: 20px; + } + + &-codeBox { + min-height: 180px; + overflow-x: hidden; + } +} diff --git a/common/containers/Tabs/SendTransaction/components/RequestPayment.tsx b/common/containers/Tabs/SendTransaction/components/RequestPayment.tsx new file mode 100644 index 00000000..cb6d267b --- /dev/null +++ b/common/containers/Tabs/SendTransaction/components/RequestPayment.tsx @@ -0,0 +1,186 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { AppState } from 'reducers'; +import translate from 'translations'; +import { IWallet } from 'libs/wallet'; +import { QRCode } from 'components/ui'; +import { getUnit, getDecimal } from 'selectors/transaction/meta'; +import { + getCurrentTo, + getCurrentValue, + ICurrentTo, + ICurrentValue +} from 'selectors/transaction/current'; +import BN from 'bn.js'; +import { NetworkConfig } from 'config/data'; +import { validNumber, validDecimal } from 'libs/validators'; +import { getGasLimit } from 'selectors/transaction'; +import { AddressField, AmountField, GasField } from 'components'; +import { SetGasLimitFieldAction } from 'actions/transaction/actionTypes/fields'; +import { buildEIP681EtherRequest, buildEIP681TokenRequest } from 'libs/values'; +import { getNetworkConfig, getSelectedTokenContractAddress } from 'selectors/config'; +import './RequestPayment.scss'; +import { reset, TReset, setCurrentTo, TSetCurrentTo } from 'actions/transaction'; + +interface OwnProps { + wallet: AppState['wallet']['inst']; +} + +interface StateProps { + unit: string; + currentTo: ICurrentTo; + currentValue: ICurrentValue; + gasLimit: SetGasLimitFieldAction['payload']; + networkConfig: NetworkConfig | undefined; + decimal: number; + tokenContractAddress: string; +} + +interface ActionProps { + reset: TReset; + setCurrentTo: TSetCurrentTo; +} + +type Props = OwnProps & StateProps & ActionProps; + +const isValidAmount = decimal => amount => validNumber(+amount) && validDecimal(amount, decimal); + +class RequestPayment extends React.Component { + public state = { + recipientAddress: '' + }; + + public componentDidMount() { + if (this.props.wallet) { + this.setWalletAsyncState(this.props.wallet); + } + this.props.reset(); + } + + public componentWillUnmount() { + this.props.reset(); + } + + public componentWillReceiveProps(nextProps: Props) { + if (nextProps.wallet && this.props.wallet !== nextProps.wallet) { + this.setWalletAsyncState(nextProps.wallet); + } + } + + public render() { + const { + tokenContractAddress, + gasLimit, + currentTo, + currentValue, + networkConfig, + unit, + decimal + } = this.props; + const chainId = networkConfig ? networkConfig.chainId : undefined; + + const eip681String = this.generateEIP681String( + currentTo.raw, + tokenContractAddress, + currentValue, + gasLimit, + unit, + decimal, + chainId + ); + + return ( +
    +
    + + +
    +
    + +
    +
    + +
    +
    + +
    +
    + + {!!eip681String.length && ( +
    +
    + +
    + +
    +
    +
    +