diff --git a/apps/finance/app/package.json b/apps/finance/app/package.json
index 2accd77b4a..ddc660758b 100644
--- a/apps/finance/app/package.json
+++ b/apps/finance/app/package.json
@@ -4,8 +4,8 @@
"private": true,
"license": "AGPL-3.0-or-later",
"dependencies": {
- "@aragon/api": "^2.0.0-beta.3",
- "@aragon/api-react": "^2.0.0-beta.1",
+ "@aragon/api": "^2.0.0-beta.4",
+ "@aragon/api-react": "^2.0.0-beta.4",
"@aragon/templates-tokens": "^1.2.0",
"@aragon/ui": "^0.40.0",
"@babel/polyfill": "^7.0.0",
diff --git a/apps/finance/app/src/components/AutoComplete/AutoComplete.js b/apps/finance/app/src/components/AutoComplete/AutoComplete.js
new file mode 100644
index 0000000000..4151587d2e
--- /dev/null
+++ b/apps/finance/app/src/components/AutoComplete/AutoComplete.js
@@ -0,0 +1,163 @@
+import React, { useState, useRef, useCallback } from 'react'
+import PropTypes from 'prop-types'
+import styled from 'styled-components'
+import { Transition, animated } from 'react-spring'
+import { ButtonBase, TextInput, springs, theme, unselectable } from '@aragon/ui'
+import { useClickOutside, useOnBlur, useArrowKeysFocus } from '../../hooks'
+import IconMagnifyingGlass from './IconMagnifyingGlass'
+
+const { accent, contentBackground, contentBorder, textPrimary } = theme
+const identity = x => x
+
+function AutoComplete({
+ forwardedRef,
+ itemButtonStyles = '',
+ items,
+ onSelect,
+ onChange,
+ renderItem = identity,
+ required,
+ value,
+ wide,
+}) {
+ const ref = forwardedRef
+ const [opened, setOpened] = useState(true)
+ const wrapRef = useRef()
+
+ const handleClose = useCallback(() => setOpened(false))
+ const handleFocus = useCallback(() => setOpened(true))
+ const handleSelect = useCallback(
+ item => e => {
+ e.preventDefault()
+ onSelect(item)
+ },
+ [onSelect]
+ )
+ const handleChange = useCallback(({ target: { value } }) => onChange(value), [
+ onChange,
+ ])
+
+ const { containerRef, handleContainerBlur } = useArrowKeysFocus(
+ '.autocomplete-items'
+ )
+ const { handleBlur } = useOnBlur(handleClose, wrapRef)
+ useClickOutside(handleClose, wrapRef)
+
+ return (
+
+
+
+
+
+
+ {show =>
+ show &&
+ (({ scale, opacity }) => (
+ `scale3d(${t},${t},1)`),
+ }}
+ >
+ {items.map(item => (
+ -
+
+ {renderItem(item, value)}
+
+
+ ))}
+
+ ))
+ }
+
+
+ )
+}
+
+AutoComplete.propTypes = {
+ forwardedRef: PropTypes.object,
+ itemButtonStyles: PropTypes.string,
+ items: PropTypes.array.isRequired,
+ onSelect: PropTypes.func.isRequired,
+ onChange: PropTypes.func.isRequired,
+ renderItem: PropTypes.func,
+ required: PropTypes.bool,
+ value: PropTypes.string,
+ wide: PropTypes.bool,
+}
+
+const Item = styled.li`
+ ${unselectable()};
+ overflow: hidden;
+ cursor: pointer;
+`
+
+const Items = styled(animated.ul)`
+ position: absolute;
+ z-index: 2;
+ top: 100%;
+ width: 100%;
+ padding: 8px 0;
+ color: ${textPrimary};
+ background: ${contentBackground};
+ border: 1px solid ${contentBorder};
+ box-shadow: 0 4px 4px 0 rgba(0, 0, 0, 0.06);
+ border-radius: 3px;
+ padding: 0;
+ margin: 0;
+ list-style: none;
+
+ & ${Item}:first-child {
+ border-top-left-radius: 3px;
+ border-top-right-radius: 3px;
+ }
+ & ${Item}:last-child {
+ border-bottom-left-radius: 3px;
+ border-bottom-right-radius: 3px;
+ }
+`
+
+const AutoCompleteMemo = React.memo(AutoComplete)
+
+export default React.forwardRef((props, ref) => (
+
+))
diff --git a/apps/finance/app/src/components/AutoComplete/AutoCompleteSelected.js b/apps/finance/app/src/components/AutoComplete/AutoCompleteSelected.js
new file mode 100644
index 0000000000..27feb3e79a
--- /dev/null
+++ b/apps/finance/app/src/components/AutoComplete/AutoCompleteSelected.js
@@ -0,0 +1,113 @@
+import React, { useRef, useCallback } from 'react'
+import PropTypes from 'prop-types'
+import { ButtonBase, theme } from '@aragon/ui'
+import AutoComplete from './AutoComplete'
+
+const identity = x => x
+const noop = () => null
+
+function AutoCompleteSelected({
+ forwardedRef,
+ itemButtonStyles,
+ items,
+ onChange,
+ onSelect, // user clicks on an item in the list and thus, selects it
+ onSelectedClick = noop, // when item is selected and user clicks on it, opens up the input for typing
+ renderItem,
+ required,
+ renderSelected = identity,
+ selected,
+ selectedButtonStyles = '',
+ value,
+ wide,
+}) {
+ const ref = forwardedRef
+ const selectedRef = useRef()
+
+ const handleSelect = useCallback(
+ selected => {
+ onSelect(selected)
+ setTimeout(() => {
+ selectedRef.current.focus()
+ }, 0)
+ },
+ [onChange]
+ )
+ const handleSelectedClick = useCallback(
+ () => {
+ onSelectedClick()
+ setTimeout(() => {
+ ref.current.select()
+ ref.current.focus()
+ }, 0)
+ },
+ [ref, selected, onChange]
+ )
+
+ if (selected) {
+ return (
+
+ {renderSelected(selected)}
+
+ )
+ }
+
+ return (
+
+ )
+}
+
+AutoCompleteSelected.propTypes = {
+ forwardedRef: PropTypes.object,
+ itemButtonStyles: PropTypes.string,
+ items: PropTypes.array.isRequired,
+ onChange: PropTypes.func.isRequired,
+ onSelect: PropTypes.func.isRequired,
+ onSelectedClick: PropTypes.func,
+ renderItem: PropTypes.func,
+ renderSelected: PropTypes.func,
+ required: PropTypes.bool,
+ selected: PropTypes.object,
+ selectedButtonStyles: PropTypes.string,
+ value: PropTypes.string,
+ wide: PropTypes.bool,
+}
+
+const AutoCompleteSelectedMemo = React.memo(AutoCompleteSelected)
+
+export default React.forwardRef((props, ref) => (
+
+))
diff --git a/apps/finance/app/src/components/AutoComplete/IconMagnifyingGlass.js b/apps/finance/app/src/components/AutoComplete/IconMagnifyingGlass.js
new file mode 100644
index 0000000000..58c69046bb
--- /dev/null
+++ b/apps/finance/app/src/components/AutoComplete/IconMagnifyingGlass.js
@@ -0,0 +1,14 @@
+import React from 'react'
+
+const IconMagnifyingGlass = React.memo(props => {
+ return (
+
+ )
+})
+
+export default IconMagnifyingGlass
diff --git a/apps/finance/app/src/components/LocalIdentitiesAutoComplete/LocalIdentitiesAutoComplete.js b/apps/finance/app/src/components/LocalIdentitiesAutoComplete/LocalIdentitiesAutoComplete.js
new file mode 100644
index 0000000000..6321b4cbb4
--- /dev/null
+++ b/apps/finance/app/src/components/LocalIdentitiesAutoComplete/LocalIdentitiesAutoComplete.js
@@ -0,0 +1,171 @@
+import React, { useState, useEffect, useCallback } from 'react'
+import { useAragonApi } from '@aragon/api-react'
+import PropTypes from 'prop-types'
+import styled from 'styled-components'
+import { EthIdenticon, IdentityBadge, theme } from '@aragon/ui'
+import AutoCompleteSelected from '../AutoComplete/AutoCompleteSelected'
+
+const withKey = item => ({ key: item.address, ...item })
+const sortAlphAsc = (a, b) => a.name.localeCompare(b.name)
+
+const LocalIdentitiesAutoComplete = React.memo(
+ React.forwardRef(function LocalIdentitiesAutoComplete(
+ { onChange, wide, value, required },
+ ref
+ ) {
+ const { api } = useAragonApi()
+ const [items, setItems] = useState([])
+ const [selected, setSelected] = useState(null)
+ const [searchTerm, setSearchTerm] = useState('')
+
+ const handleSearch = useCallback(
+ async term => {
+ if (term.length < 3) {
+ setItems([])
+ return
+ }
+ const items = await api.searchIdentities(term).toPromise()
+ setItems(items.map(withKey).sort(sortAlphAsc))
+ },
+ [api]
+ )
+ const handleChange = useCallback(
+ value => {
+ setSearchTerm(value)
+ handleSearch(value)
+ onChange(value)
+ },
+ [onChange]
+ )
+ const handleSelect = useCallback(
+ selected => {
+ const { name, address } = selected
+ setSearchTerm(name)
+ handleSearch(name)
+ setSelected(selected)
+ onChange(address)
+ },
+ [onChange]
+ )
+ const handleSelectedClick = () => {
+ setSelected(null)
+ onChange(selected.name)
+ }
+ const renderItem = useCallback(({ address, name }, searchTerm) => {
+ if (searchTerm.indexOf('0x') === 0) {
+ return (
+
+ )
+ }
+ return (
+
+ )
+ })
+ const renderSelected = useCallback(({ address, name }) => {
+ return (
+
+ )
+ })
+
+ useEffect(() => {
+ const effect = async () => {
+ // reset
+ if (value === '') {
+ setSelected(null)
+ setSearchTerm(value)
+ handleSearch(value)
+ return
+ }
+ // value coming from up the tree not from typing
+ if (searchTerm === '') {
+ const exists = await api.searchIdentities(value).toPromise()
+ if (exists && exists.length === 1) {
+ const item = exists[0]
+ if (
+ item.name.toLowerCase() === value.toLowerCase() ||
+ item.address.toLowerCase() === value.toLowerCase()
+ ) {
+ setSelected(item)
+ setSearchTerm(item.name)
+ handleSearch(item.name)
+ return
+ }
+ }
+ setSearchTerm(value)
+ }
+ }
+ effect()
+ }, [selected, value, api])
+
+ return (
+
+ )
+ })
+)
+
+LocalIdentitiesAutoComplete.propTypes = {
+ onChange: PropTypes.func.isRequired,
+ required: PropTypes.bool,
+ value: PropTypes.string,
+ wide: PropTypes.bool,
+}
+
+const Option = styled.div`
+ padding: 8px;
+ display: grid;
+ grid-template-columns: auto minmax(140px, 1fr);
+ grid-gap: 8px;
+ align-items: center;
+`
+
+const Name = styled.div`
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: inline-block;
+ text-align: left;
+ color: #000;
+`
+
+export default LocalIdentitiesAutoComplete
diff --git a/apps/finance/app/src/components/NewTransfer/Withdrawal.js b/apps/finance/app/src/components/NewTransfer/Withdrawal.js
index 39522a29a9..3d2cfd1089 100644
--- a/apps/finance/app/src/components/NewTransfer/Withdrawal.js
+++ b/apps/finance/app/src/components/NewTransfer/Withdrawal.js
@@ -11,6 +11,7 @@ import {
theme,
unselectable,
} from '@aragon/ui'
+import LocalIdentitiesAutoComplete from '../LocalIdentitiesAutoComplete/LocalIdentitiesAutoComplete'
import { toDecimals } from '../../lib/math-utils'
import { addressPattern, isAddress } from '../../lib/web3-utils'
@@ -67,11 +68,11 @@ class Withdrawal extends React.Component {
handleSelectToken = index => {
this.setState({ selectedToken: index })
}
- handleRecipientUpdate = event => {
+ handleRecipientUpdate = value => {
this.setState({
recipient: {
error: NO_ERROR,
- value: event.target.value,
+ value,
},
})
}
@@ -137,8 +138,11 @@ class Withdrawal extends React.Component {
return tokens.length ? (