Browse Source

Merge pull request #65 from loeck/master

Scrollbar, ReceiveBox, QrCode Reader, RecipientAddress... ~o~
master
Loëck Vézien 7 years ago
committed by GitHub
parent
commit
29726d8f14
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      electron-builder.yml
  2. 1
      package.json
  3. 4
      src/components/AccountPage.js
  4. 14
      src/components/ReceiveBox.js
  5. 87
      src/components/RecipientAddress/index.js
  6. 40
      src/components/RecipientAddress/stories.js
  7. 9
      src/components/base/Button/index.js
  8. 43
      src/components/base/GrowScroll/index.js
  9. 22
      src/components/base/GrowScroll/stories.js
  10. 1
      src/components/base/Icon/index.js
  11. 11
      src/components/base/Modal/index.js
  12. 53
      src/components/base/Select/index.js
  13. 69
      src/components/modals/Receive.js
  14. 66
      src/components/modals/Send.js
  15. 2
      src/main/app.js
  16. 17
      src/styles/global.js
  17. 1
      static/i18n/en/translation.yml
  18. 1
      static/i18n/fr/translation.yml
  19. 29
      yarn.lock

5
electron-builder.yml

@ -1,5 +1,10 @@
appId: com.electron.ledger
protocols:
name: Ledger Wallet Desktop
schemes:
- ledgerhq
mac:
category: public.app-category.utilities
linux:

1
package.json

@ -71,6 +71,7 @@
"react-i18next": "^7.3.4",
"react-mortal": "^3.0.1",
"react-motion": "^0.5.2",
"react-qr-reader": "^2.0.1",
"react-redux": "^5.0.6",
"react-router": "^4.2.0",
"react-router-dom": "^4.2.2",

4
src/components/AccountPage.js

@ -51,7 +51,7 @@ class AccountPage extends PureComponent<Props> {
<Box horizontal align="center" justify="flex-end" grow flow={20}>
<Box>
<Button primary onClick={() => openModal(MODAL_SEND, { account })}>
<Box horizontal flow={2}>
<Box horizontal flow={2} align="center">
<Box>
<Icon name="upload" />
</Box>
@ -61,7 +61,7 @@ class AccountPage extends PureComponent<Props> {
</Box>
<Box>
<Button primary onClick={() => openModal(MODAL_RECEIVE, { account })}>
<Box horizontal flow={2}>
<Box horizontal flow={2} align="center">
<Box>
<Icon name="download" />
</Box>

14
src/components/ReceiveBox.js

@ -10,6 +10,7 @@ import CopyToClipboard from 'components/base/CopyToClipboard'
import Text from 'components/base/Text'
type Props = {
amount?: string,
address: string,
}
@ -26,9 +27,10 @@ const AddressBox = styled(Box).attrs({
`
const Action = styled(Box).attrs({
flow: 1,
flex: 1,
align: 'center',
color: 'mouse',
flex: 1,
flow: 1,
fontSize: 0,
})`
font-weight: bold;
@ -41,10 +43,10 @@ const Action = styled(Box).attrs({
}
`
const ReceiveBox = ({ address }: Props) => (
const ReceiveBox = ({ amount, address }: Props) => (
<Box flow={3}>
<Box align="center">
<QRCode size={150} data={address} />
<QRCode size={150} data={`bitcoin:${address}${amount ? `?amount=${amount}` : ''}`} />
</Box>
<Box align="center" flow={2}>
<Text fontSize={1}>{'Current address'}</Text>
@ -72,4 +74,8 @@ const ReceiveBox = ({ address }: Props) => (
</Box>
)
ReceiveBox.defaultProps = {
amount: undefined,
}
export default ReceiveBox

87
src/components/RecipientAddress/index.js

@ -0,0 +1,87 @@
// @flow
import React, { PureComponent, Fragment } from 'react'
import styled from 'styled-components'
import QrReader from 'react-qr-reader'
import noop from 'lodash/noop'
import Box from 'components/base/Box'
import Icon from 'components/base/Icon'
import Input from 'components/base/Input'
const IconQrCode = ({ onClick }: { onClick: Function }) => (
<Box color="steel" style={{ position: 'absolute', right: 15 }}>
<Icon fontSize={30} name="qrcode" style={{ cursor: 'pointer' }} onClick={onClick} />
</Box>
)
const InputAddress = styled(Input).attrs({
type: 'text',
})`
padding-right: ${p => p.withQrCode && '55px'};
`
const WrapperQrCode = styled(Box)`
margin-top: 10px;
position: absolute;
right: 15px;
top: 100%;
`
type Props = {
value: string,
onChange: Function,
qrCodeSize: number,
withQrCode: boolean,
}
type State = {
qrReaderOpened: boolean,
}
class RecipientAddress extends PureComponent<Props, State> {
static defaultProps = {
value: '',
onChange: noop,
qrCodeSize: 200,
withQrCode: true,
}
state = {
qrReaderOpened: false,
}
handleClickQrCode = () =>
this.setState(prev => ({
qrReaderOpened: !prev.qrReaderOpened,
}))
handleScanQrCode = (data: string) => data !== null && this.props.onChange(data)
render() {
const { onChange, qrCodeSize, withQrCode, value } = this.props
const { qrReaderOpened } = this.state
return (
<Box relative justify="center">
<InputAddress value={value} withQrCode={withQrCode} onChange={onChange} />
{withQrCode && (
<Fragment>
<IconQrCode onClick={this.handleClickQrCode} />
{qrReaderOpened && (
<WrapperQrCode>
<QrReader
onScan={this.handleScanQrCode}
onError={noop}
style={{ height: qrCodeSize, width: qrCodeSize }}
/>
</WrapperQrCode>
)}
</Fragment>
)}
</Box>
)
}
}
export default RecipientAddress

40
src/components/RecipientAddress/stories.js

@ -0,0 +1,40 @@
// @flow
import React, { PureComponent } from 'react'
import { storiesOf } from '@storybook/react'
import { boolean } from '@storybook/addon-knobs'
import RecipientAddress from 'components/RecipientAddress'
const stories = storiesOf('RecipientAddress', module)
type State = {
value: any,
}
class Wrapper extends PureComponent<any, State> {
state = {
value: '',
}
handleChange = item => this.setState({ value: item })
render() {
const { render } = this.props
const { value } = this.state
return render({ onChange: this.handleChange, value })
}
}
stories.add('basic', () => (
<Wrapper
render={({ onChange, value }) => (
<RecipientAddress
withQrCode={boolean('withQrCode', true)}
onChange={onChange}
value={value}
/>
)}
/>
))

9
src/components/base/Button/index.js

@ -4,6 +4,7 @@ import React from 'react'
import styled from 'styled-components'
import { borderColor, borderWidth, space, fontSize, fontWeight, color } from 'styled-system'
import Box from 'components/base/Box'
import Icon from 'components/base/Icon'
const Base = styled.button`
@ -27,7 +28,13 @@ type Props = {
}
const Button = ({ primary, children, icon, ...props }: Props) => {
children = icon ? <Icon name={icon} /> : children
children = icon ? (
<Box align="center" justify="center">
<Icon name={icon} />
</Box>
) : (
children
)
props = {
...props,

43
src/components/base/GrowScroll/index.js

@ -7,43 +7,19 @@ import noop from 'lodash/noop'
import Box from 'components/base/Box'
type Props = {
maxHeight?: number | string,
children: any,
offsetLimit: Object,
full: boolean,
maxHeight?: number,
onUpdate: Function,
}
class GrowScroll extends PureComponent<Props> {
static defaultProps = {
full: false,
onUpdate: noop,
offsetLimit: {
y: {
max: -3,
min: 3,
},
},
}
componentDidMount() {
const { offsetLimit } = this.props
if (this._scrollbar) {
this._scrollbar.addListener(function onScroll({ limit, offset }) {
if (limit.y > 0) {
const maxY = limit.y + offsetLimit.y.max
const minY = offsetLimit.y.min
if (offset.y > maxY) {
this.scrollTo(offset.x, maxY)
}
if (offset.y < minY) {
this.scrollTo(offset.x, minY)
}
}
})
}
this.handleUpdate(this.props)
}
@ -60,10 +36,19 @@ class GrowScroll extends PureComponent<Props> {
_scrollbar = undefined
render() {
const { onUpdate, children, maxHeight, ...props } = this.props
const { onUpdate, children, maxHeight, full, ...props } = this.props
return (
<Box grow relative>
<Box
{...(full
? {
sticky: true,
}
: {
grow: true,
relative: true,
})}
>
<Scrollbar
damping={1}
style={{

22
src/components/base/GrowScroll/stories.js

@ -1,13 +1,25 @@
import React from 'react'
import { storiesOf } from '@storybook/react'
import { boolean } from '@storybook/addon-knobs'
import Box from 'components/base/Box'
import GrowScroll from 'components/base/GrowScroll'
const stories = storiesOf('GrowScroll', module)
stories.add('basic', () => (
<Box style={{ height: 400, border: '1px solid black' }}>
<GrowScroll>{[...Array(1000).keys()].map(v => <div key={v}>{v}</div>)}</GrowScroll>
</Box>
))
stories.add('basic', () => {
const reverseColor = boolean('reverseColor', false)
return (
<Box
borderWidth={1}
borderColor="night"
bg={reverseColor ? 'night' : 'white'}
color={reverseColor ? 'white' : 'night'}
>
<GrowScroll maxHeight={400}>
{[...Array(1000).keys()].map(v => <div key={v}>{v}</div>)}
</GrowScroll>
</Box>
)
})

1
src/components/base/Icon/index.js

@ -8,6 +8,7 @@ import FontAwesomeIcon from '@fortawesome/react-fontawesome'
const Container = styled.span`
${fontSize};
${color};
display: inline-flex;
position: relative;
`

11
src/components/base/Modal/index.js

@ -14,6 +14,7 @@ import { rgba } from 'styles/helpers'
import { closeModal, isModalOpened, getModalData } from 'reducers/modals'
import Box from 'components/base/Box'
import GrowScroll from 'components/base/GrowScroll'
import Icon from 'components/base/Icon'
type Props = {
@ -52,8 +53,8 @@ const Container = styled(Box).attrs({
pointerEvents: p.isVisible ? 'auto' : 'none',
}),
})`
overflow: scroll;
position: fixed;
top: ${process.platform === 'darwin' ? 40 : 0}px;
z-index: 20;
`
@ -126,9 +127,11 @@ export class Modal extends PureComponent<Props> {
{(m, isVisible) => (
<Container isVisible={isVisible}>
<Backdrop op={m.opacity} onClick={preventBackdropClick ? undefined : onClose} />
<Wrapper preventSideMargin={preventSideMargin} op={m.opacity} offset={m.y}>
{render({ data, onClose })}
</Wrapper>
<GrowScroll full align="center">
<Wrapper preventSideMargin={preventSideMargin} op={m.opacity} offset={m.y}>
{render({ data, onClose })}
</Wrapper>
</GrowScroll>
</Container>
)}
</Mortal>

53
src/components/base/Select/index.js

@ -4,7 +4,6 @@ import React, { PureComponent } from 'react'
import Downshift from 'downshift'
import styled from 'styled-components'
import { space } from 'styled-system'
import get from 'lodash/get'
import type { Element } from 'react'
@ -18,18 +17,19 @@ import Text from 'components/base/Text'
import Triangles from './Triangles'
type Props = {
fuseOptions?: Object,
highlight?: boolean,
items: Array<any>,
value?: Object | null,
itemToString?: Function,
keyProp?: string,
maxHeight?: number,
onChange?: Function,
fuseOptions?: Object,
highlight?: boolean,
searchable?: boolean,
placeholder?: string,
renderHighlight?: string => Element<*>,
renderSelected?: any => Element<*>,
renderItem?: (*) => Element<*>,
keyProp?: string,
renderSelected?: any => Element<*>,
searchable?: boolean,
value?: Object | null,
}
const Container = styled(Box).attrs({ relative: true, color: 'steel' })``
@ -102,24 +102,22 @@ const IconSelected = styled(Box).attrs({
font-size: 5px;
width: 15px;
opacity: ${p => (p.selected ? 1 : 0)};
// add top for center icon
> * {
top: 1px;
}
`
class Select extends PureComponent<Props> {
static defaultProps = {
itemToString: (item: Object) => item && item.name,
keyProp: undefined,
maxHeight: 300,
}
_scrollToSelectedItem = true
_oldHighlightedIndex = 0
_useKeyboard = false
_children = {}
renderItems = (items: Array<Object>, selectedItem: any, downshiftProps: Object) => {
const { renderItem, keyProp } = this.props
const { renderItem, maxHeight, keyProp } = this.props
const { getItemProps, highlightedIndex } = downshiftProps
const selectedItemIndex = items.indexOf(selectedItem)
@ -128,29 +126,36 @@ class Select extends PureComponent<Props> {
<Dropdown>
{items.length ? (
<GrowScroll
maxHeight={300}
maxHeight={maxHeight}
onUpdate={scrollbar => {
const { contentEl } = scrollbar
const children = get(contentEl, 'children[0].children[0].children', {})
const currentHighlighted = children[highlightedIndex]
const currentSelectedItem = children[selectedItemIndex]
const currentHighlighted = this._children[highlightedIndex]
const currentSelectedItem = this._children[selectedItemIndex]
if (this._useKeyboard && currentHighlighted) {
scrollbar.scrollIntoView(currentHighlighted, {
alignToTop: false,
alignToTop: highlightedIndex < this._oldHighlightedIndex,
offsetTop: -1,
onlyScrollIfNeeded: true,
})
} else if (this._scrollToSelectedItem && currentSelectedItem) {
scrollbar.scrollIntoView(currentSelectedItem, {
alignToTop: false,
})
window.requestAnimationFrame(() =>
scrollbar.scrollIntoView(currentSelectedItem, {
offsetTop: -1,
}),
)
this._scrollToSelectedItem = false
}
this._oldHighlightedIndex = highlightedIndex
}}
>
{items.map((item, i) => (
<ItemWrapper key={keyProp ? item[keyProp] : item.key} {...getItemProps({ item })}>
<ItemWrapper
key={keyProp ? item[keyProp] : item.key}
innerRef={n => (this._children[i] = n)}
{...getItemProps({ item })}
>
<Item highlighted={i === highlightedIndex} horizontal flow={10}>
<Box grow>
{renderItem ? (

69
src/components/modals/Receive.js

@ -1,14 +1,19 @@
// @flow
import React, { PureComponent } from 'react'
import React, { PureComponent, Fragment } from 'react'
import { translate } from 'react-i18next'
import get from 'lodash/get'
import { MODAL_RECEIVE } from 'constants'
import Box from 'components/base/Box'
import Button from 'components/base/Button'
import Input from 'components/base/Input'
import Label from 'components/base/Label'
import Modal, { ModalBody } from 'components/base/Modal'
import Text from 'components/base/Text'
import ReceiveBox from 'components/ReceiveBox'
import SelectAccount from 'components/SelectAccount'
import Text from 'components/base/Text'
import type { Account as AccountType, T } from 'types/common'
@ -18,10 +23,12 @@ type Props = {
type State = {
account: AccountType | null,
amount: string,
}
const defaultState = {
account: null,
amount: '',
}
class ReceiveModal extends PureComponent<Props, State> {
@ -29,34 +36,64 @@ class ReceiveModal extends PureComponent<Props, State> {
...defaultState,
}
handleChangeAccount = account => {
this.setState({ account })
getAccount(data) {
const { account } = this.state
return account || get(data, 'account')
}
handleChangeInput = key => value =>
this.setState({
[key]: value,
})
handleClose = () =>
this.setState({
...defaultState,
})
render() {
const { account } = this.state
const { amount } = this.state
const { t } = this.props
return (
<Modal
name={MODAL_RECEIVE}
onClose={this.handleClose}
render={({ data, onClose }) => (
<ModalBody onClose={onClose} flow={3}>
<Text fontSize={4} color="steel">
{t('receive.modalTitle')}
</Text>
<SelectAccount
value={account || get(data, 'account')}
onChange={this.handleChangeAccount}
/>
</ModalBody>
)}
render={({ data, onClose }) => {
const account = this.getAccount(data)
return (
<ModalBody onClose={onClose} flow={3}>
<Text fontSize={4} color="steel">
{t('receive.title')}
</Text>
<Box flow={1}>
<Label>Account</Label>
<SelectAccount value={account} onChange={this.handleChangeInput('account')} />
</Box>
{account &&
account.data && (
<Fragment>
<Box flow={1}>
<Label>Request amount</Label>
<Input
type="number"
min={0}
max={account.data.balance / 1e8}
onChange={this.handleChangeInput('amount')}
/>
</Box>
<ReceiveBox amount={amount} address={get(account, 'data.address', '')} />
</Fragment>
)}
<Box horizontal justify="center">
<Button primary onClick={onClose}>
Close
</Button>
</Box>
</ModalBody>
)
}}
/>
)
}

66
src/components/modals/Send.js

@ -1,23 +1,24 @@
// @flow
import React, { Fragment, PureComponent } from 'react'
import styled from 'styled-components'
import { translate } from 'react-i18next'
import get from 'lodash/get'
import type { T } from 'types/common'
import { MODAL_SEND } from 'constants'
import Box from 'components/base/Box'
import Button from 'components/base/Button'
import Input from 'components/base/Input'
import Label from 'components/base/Label'
import Modal, { ModalBody } from 'components/base/Modal'
import RecipientAddress from 'components/RecipientAddress'
import SelectAccount from 'components/SelectAccount'
const Label = styled.label`
display: block;
text-transform: uppercase;
`
import Text from 'components/base/Text'
const Steps = {
amount: (props: Object) => (
amount: ({ t, ...props }: Object) => (
<form
onSubmit={(e: SyntheticEvent<HTMLFormElement>) => {
e.preventDefault()
@ -33,20 +34,33 @@ const Steps = {
props.onChangeStep('summary')
}}
>
<div>amount</div>
<div>
<Label>Account to debit</Label>
<SelectAccount onChange={props.onChangeInput('account')} value={props.value.account} />
</div>
<div>
<Label>Recipient address</Label>
<Input onChange={props.onChangeInput('address')} value={props.value.address} />
</div>
<div>
<Label>Amount</Label>
<Input onChange={props.onChangeInput('amount')} value={props.value.amount} />
</div>
<Button type="submit">Next</Button>
<Box flow={3}>
<Text fontSize={4} color="steel">
{t('send.title')}
</Text>
<Box flow={1}>
<Label>Account to debit</Label>
<SelectAccount onChange={props.onChangeInput('account')} value={props.value.account} />
</Box>
<Box flow={1}>
<Label>Recipient address</Label>
<RecipientAddress onChange={props.onChangeInput('address')} value={props.value.address} />
</Box>
<Box flow={1}>
<Label>Amount</Label>
<Input onChange={props.onChangeInput('amount')} value={props.value.amount} />
</Box>
<Box horizontal align="center">
<Box grow>
<Text>Cancel</Text>
</Box>
<Box justify="flex-end">
<Button type="submit" primary>
Next
</Button>
</Box>
</Box>
</Box>
</form>
),
summary: (props: Object) => (
@ -72,6 +86,10 @@ type State = {
step: Step,
}
type Props = {
t: T,
}
const defaultState = {
inputValue: {
account: null,
@ -81,13 +99,14 @@ const defaultState = {
step: 'amount',
}
class Send extends PureComponent<{}, State> {
class Send extends PureComponent<Props, State> {
state = {
...defaultState,
}
getStepProps(data: any) {
const { inputValue, step } = this.state
const { t } = this.props
const props = (predicate, props) => (predicate ? props : {})
@ -103,6 +122,7 @@ class Send extends PureComponent<{}, State> {
value: inputValue,
}),
onChangeStep: this.handleChangeStep,
t,
}
}
@ -146,4 +166,4 @@ class Send extends PureComponent<{}, State> {
}
}
export default Send
export default translate()(Send)

2
src/main/app.js

@ -78,6 +78,8 @@ const installExtensions = async () => {
).catch(console.log) // eslint-disable-line
}
app.setAsDefaultProtocolClient('ledgerhq')
app.on('ready', async () => {
if (__DEV__) {
await installExtensions()

17
src/styles/global.js

@ -45,22 +45,13 @@ injectGlobal`
font-style: italic;
}
.scrollbar-thumb-y {
width: 5px !important;
}
.scrollbar-thumb-x {
height: 5px !important;
.scrollbar-thumb {
background: rgba(102, 102, 102, 0.5) !important;
padding: 2px;
background-clip: content-box !important;
}
.scrollbar-track {
background: transparent !important;
transition: opacity 0.2s ease-in-out !important;
}
.scrollbar-track-y {
right: 2px !important;
width: 5px !important;
}
.scrollbar-track-x {
bottom: 2px !important;
height: 5px !important;
}
`

1
static/i18n/en/translation.yml

@ -18,7 +18,6 @@ send:
receive:
title: Receive
modalTitle: Receive funds
addAccount:
title: Add account

1
static/i18n/fr/translation.yml

@ -18,7 +18,6 @@ send:
receive:
title: Recevoir
modalTitle: Recevoir des fonds
addAccount:
title: Ajouter un compte

29
yarn.lock

@ -5409,6 +5409,10 @@ jsprim@^1.2.2:
json-schema "0.2.3"
verror "1.10.0"
"jsqr@git+https://github.com/cozmo/jsQR.git":
version "1.0.1"
resolved "git+https://github.com/cozmo/jsQR.git#1fb946a235abdc7709f04cd0e4aa316a3b6eae70"
jsx-ast-utils@^2.0.0, jsx-ast-utils@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.0.1.tgz#e801b1b39985e20fffc87b40e3748080e2dcac7f"
@ -7401,6 +7405,14 @@ react-portal@^4.0.0:
dependencies:
prop-types "^15.5.8"
react-qr-reader@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/react-qr-reader/-/react-qr-reader-2.0.1.tgz#f7be785e8c880d7e68423fc129802994f70b6b58"
dependencies:
jsqr "https://github.com/cozmo/jsQR.git"
prop-types "^15.5.8"
webrtc-adapter "^5.0.6"
react-redux@^5.0.5, react-redux@^5.0.6:
version "5.0.6"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.6.tgz#23ed3a4f986359d68b5212eaaa681e60d6574946"
@ -7929,6 +7941,12 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
hash-base "^2.0.0"
inherits "^2.0.1"
rtcpeerconnection-shim@^1.1.13:
version "1.2.5"
resolved "https://registry.yarnpkg.com/rtcpeerconnection-shim/-/rtcpeerconnection-shim-1.2.5.tgz#bfbae97e265ad05377e6fed1cfcb7f5880ce6000"
dependencies:
sdp "^2.2.0"
run-async@^2.2.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0"
@ -7988,6 +8006,10 @@ schema-utils@^0.4.2:
ajv "^5.0.0"
ajv-keywords "^2.1.0"
sdp@^2.2.0, sdp@^2.3.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/sdp/-/sdp-2.5.0.tgz#b15a0b1dfd0a38f6a8c780e58f8c8fe73c29ffe5"
select-hose@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
@ -9282,6 +9304,13 @@ webpack@^3.10.0:
webpack-sources "^1.0.1"
yargs "^8.0.2"
webrtc-adapter@^5.0.6:
version "5.0.6"
resolved "https://registry.yarnpkg.com/webrtc-adapter/-/webrtc-adapter-5.0.6.tgz#7946fca194dadf869bb6c8cae1011dfda03f40c7"
dependencies:
rtcpeerconnection-shim "^1.1.13"
sdp "^2.3.0"
websocket-driver@>=0.5.1:
version "0.7.0"
resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.0.tgz#0caf9d2d755d93aee049d4bdd0d3fe2cca2a24eb"

Loading…
Cancel
Save