diff --git a/package.json b/package.json index 85e2aa74..8cd02752 100644 --- a/package.json +++ b/package.json @@ -49,8 +49,9 @@ "@ledgerhq/hw-app-eth": "^4.7.3", "@ledgerhq/hw-transport": "^4.7.3", "@ledgerhq/hw-transport-node-hid": "^4.7.6", - "@ledgerhq/wallet-common": "^0.12.0", + "@ledgerhq/wallet-common": "^0.13.2", "axios": "^0.18.0", + "babel-runtime": "^6.26.0", "bcryptjs": "^2.4.3", "bitcoinjs-lib": "^3.3.2", "bs58check": "^2.1.1", @@ -58,23 +59,23 @@ "cross-env": "^5.1.4", "d3": "^5.0.0", "debug": "^3.1.0", - "downshift": "^1.31.2", + "downshift": "^1.31.6", "electron-store": "^1.3.0", "electron-updater": "^2.21.4", "fuse.js": "^3.2.0", "history": "^4.7.2", - "i18next": "^10.6.0", + "i18next": "^11.1.1", "i18next-node-fs-backend": "^1.0.0", "ledger-test-library": "KhalilBellakrid/ledger-test-library-nodejs#7d37482", "lodash": "^4.17.5", "moment": "^2.22.0", "object-path": "^0.11.4", "qrcode": "^1.2.0", - "query-string": "^6.0.0", - "raven": "^2.4.2", - "raven-js": "^3.24.0", - "react": "^16.3.0", - "react-dom": "^16.3.0", + "qs": "^6.5.1", + "raven": "^2.5.0", + "raven-js": "^3.24.1", + "react": "^16.3.1", + "react-dom": "^16.3.1", "react-i18next": "^7.5.1", "react-mortal": "^3.2.0", "react-motion": "^0.5.2", @@ -92,28 +93,28 @@ "source-map-support": "^0.5.4", "styled-components": "^3.2.5", "styled-system": "^2.2.1", - "tippy.js": "^2.4.1", + "tippy.js": "^2.5.0", "ws": "^5.1.1" }, "devDependencies": { "@babel/core": "7.0.0-beta.42", + "@babel/polyfill": "7.0.0-beta.42", "@babel/preset-env": "7.0.0-beta.42", "@babel/preset-flow": "7.0.0-beta.42", "@babel/preset-react": "7.0.0-beta.42", "@babel/preset-stage-0": "7.0.0-beta.42", - "@storybook/addon-actions": "^3.4.0", - "@storybook/addon-knobs": "^3.4.0", - "@storybook/addon-links": "^3.4.0", - "@storybook/addon-options": "^3.4.0", - "@storybook/addons": "^3.4.0", - "@storybook/react": "^3.4.0", + "@storybook/addon-actions": "^3.4.1", + "@storybook/addon-knobs": "^3.4.1", + "@storybook/addon-links": "^3.4.1", + "@storybook/addon-options": "^3.4.1", + "@storybook/addons": "^3.4.1", + "@storybook/react": "^3.4.1", "babel-core": "7.0.0-bridge.0", "babel-eslint": "^8.2.1", "babel-jest": "^22.4.3", "babel-loader": "^8.0.0-beta.2", "babel-plugin-module-resolver": "^3.1.1", "babel-plugin-styled-components": "^1.5.0", - "babel-polyfill": "^6.26.0", "chalk": "^2.3.1", "chance": "^1.0.13", "concurrently": "^3.5.1", @@ -137,14 +138,14 @@ "husky": "^0.14.3", "jest": "^22.4.3", "js-yaml": "^3.10.0", - "lint-staged": "^7.0.2", + "lint-staged": "^7.0.4", "node-loader": "^0.6.0", "prettier": "^1.11.1", "react-hot-loader": "^4.0.1", - "react-test-renderer": "^16.3.0", - "webpack": "^4.4.1", + "react-test-renderer": "^16.3.1", + "webpack": "^4.5.0", "webpack-bundle-analyzer": "^2.11.1", - "webpack-cli": "^2.0.13", + "webpack-cli": "^2.0.14", "yaml-loader": "^0.5.0" } } diff --git a/src/components/Breadcrumb/Step.js b/src/components/Breadcrumb/Step.js index e7a94ba7..54521260 100644 --- a/src/components/Breadcrumb/Step.js +++ b/src/components/Breadcrumb/Step.js @@ -1,16 +1,21 @@ // @flow -import React, { Fragment } from 'react' +import React from 'react' import styled from 'styled-components' import Box from 'components/base/Box' -const RADIUS = 17 +import IconCheck from 'icons/Check' +import IconCross from 'icons/Cross' + +const RADIUS = 18 const Wrapper = styled(Box).attrs({ - align: 'center', - justify: 'center', - color: p => (p.isActive ? 'wallet' : 'fog'), + alignItems: 'center', + color: p => + ['active', 'valid'].includes(p.status) ? 'wallet' : p.status === 'error' ? 'alertRed' : 'grey', + grow: true, + justifyContent: 'center', })` width: ${RADIUS}px; flex-shrink: 0; @@ -18,44 +23,20 @@ const Wrapper = styled(Box).attrs({ font-size: 9px; ` -const Number = styled(Box).attrs({ - align: 'center', - justify: 'center', +const StepNumber = styled(Box).attrs({ + alignItems: 'center', + justifyContent: 'center', color: 'white', - bg: p => (p.isActive ? 'wallet' : 'fog'), + bg: p => + ['active', 'valid'].includes(p.status) ? 'wallet' : p.status === 'error' ? 'alertRed' : 'fog', ff: 'Rubik|Regular', })` - width: ${RADIUS}px; - height: ${RADIUS}px; border-radius: 50%; font-size: 10px; + height: ${RADIUS}px; line-height: 10px; - box-shadow: ${p => `0 0 0 ${p.isActive ? 4 : 0}px ${p.theme.colors.lightGrey}`}; - transition: all ease-in-out 0.1s ${p => (p.isActive ? 0.4 : 0)}s; -` - -const Bar = styled.div` - height: 2px; - background: ${p => p.theme.colors.fog}; - flex-grow: 1; - max-width: 100px; - position: relative; - margin-top: -2px; - - &:after { - background: ${p => p.theme.colors.pearl}; - content: ''; - display: block; - left: 0; - right: 0; - top: 0; - bottom: 0; - position: absolute; - background: ${p => p.theme.colors.wallet}; - transition: transform ease-in-out 0.4s; - transform-origin: center left; - transform: scaleX(${p => (p.isActive ? 1 : 0)}); - } + transition: all ease-in-out 0.1s ${p => (['active', 'valid'].includes(p.status) ? 0.4 : 0)}s; + width: ${RADIUS}px; ` const Label = styled(Box).attrs({ @@ -63,27 +44,31 @@ const Label = styled(Box).attrs({ ff: 'Museo Sans|Bold', })` position: absolute; - margin-top: 27px; - transition: color ease-in-out 0.1s ${p => (p.isActive ? 0.4 : 0)}s; + margin-top: 23px; + transition: color ease-in-out 0.1s ${p => (['active', 'valid'].includes(p.status) ? 0.4 : 0)}s; ` type Props = { number: number, - isActive: boolean, - isFirst: boolean, + status: 'next' | 'active' | 'valid' | 'error' | 'disable', children: any, } function Step(props: Props) { - const { number, isActive, isFirst, children } = props + const { number, status, children } = props return ( - - {!isFirst && } - - {number} - - - + + + {status === 'active' || status === 'next' ? ( + number + ) : status === 'valid' ? ( + + ) : ( + + )} + + + ) } diff --git a/src/components/Breadcrumb/index.js b/src/components/Breadcrumb/index.js index 0f8fd615..4fb9b983 100644 --- a/src/components/Breadcrumb/index.js +++ b/src/components/Breadcrumb/index.js @@ -8,36 +8,116 @@ import styled from 'styled-components' import Box from 'components/base/Box' import Step from './Step' -type Props = { - items: Array, - currentStep: number | string, -} - const Wrapper = styled(Box).attrs({ horizontal: true, - align: 'center', - justify: 'center', + alignItems: 'center', + justifyContent: 'center', + relative: true, })` margin-bottom: 25px; + z-index: 2; +` + +const Bar = styled.div` + background: ${p => p.theme.colors.fog}; + flex-grow: 1; + height: 1px; + left: ${p => p.start}%; + position: absolute; + right: ${p => p.end}%; + top: 8px; + z-index: 1; + + &:after, + &:before { + bottom: 0; + content: ''; + display: block; + left: 0; + position: absolute; + right: auto; + top: 0; + transition: right ease-in-out 0.4s; + } + + &:after { + background: ${p => p.theme.colors.wallet}; + right: ${p => (p.current === 0 ? 0 : `${p.current}%`)}; + z-index: 1; + } + + &:before { + background: ${p => p.theme.colors.fog}; + left: ${p => (p.disabled ? `${p.disabled[0]}%` : 0)}; + right: ${p => (p.disabled ? `${p.disabled[1]}%` : 'auto')}; + z-index: 2; + } ` +const indexToPurcent = (index, itemsLength) => 100 - 100 / (itemsLength - 1) * parseInt(index, 10) + +type Props = { + currentStep: number | string, + items: Array, + stepsDisabled: Array, + stepsErrors: Array, +} + class Breadcrumb extends PureComponent { + static defaultProps = { + stepsDisabled: [], + stepsErrors: [], + } + render() { - const { items, currentStep, ...props } = this.props + const { items, stepsDisabled, stepsErrors, currentStep, ...props } = this.props + const itemsLength = items.length + const start = 100 / itemsLength / 2 + return ( - + - {items.map((item, i) => ( - - {item.label} - - ))} + {items.map((item, i) => { + let status = 'next' + + const stepIndex = parseInt(currentStep, 10) + + if (i === stepIndex) { + status = 'active' + } + + if (i < stepIndex) { + status = 'valid' + } + + if (stepsErrors.includes(i)) { + status = 'error' + } + + if (stepsDisabled.includes(i)) { + status = 'disable' + } + + return ( + + {item.label} + + ) + })} + 0 + ? [ + stepsDisabled[0] === 0 ? 0 : indexToPurcent(stepsDisabled[0] + 1, itemsLength), + indexToPurcent(stepsDisabled[stepsDisabled.length - 1], itemsLength), + ] + : null + } + current={!currentStep ? 100 : indexToPurcent(currentStep, itemsLength)} + /> ) } diff --git a/src/components/Breadcrumb/stories.js b/src/components/Breadcrumb/stories.js index 618b4768..7807f0b7 100644 --- a/src/components/Breadcrumb/stories.js +++ b/src/components/Breadcrumb/stories.js @@ -2,23 +2,31 @@ import React from 'react' import { storiesOf } from '@storybook/react' -import { number } from '@storybook/addon-knobs' +import { array, number } from '@storybook/addon-knobs' import Breadcrumb from 'components/Breadcrumb' const stories = storiesOf('Components', module) stories.add('Breadcrumb', () => ( - +
+ Number(a))} + stepsErrors={array('stepsErrors', []).map(a => Number(a))} + items={[ + { label: 'Amount' }, + { label: 'Summary' }, + { label: 'Secure validation' }, + { label: 'Confirmation' }, + ]} + /> +
)) diff --git a/src/components/CurrentAddress/index.js b/src/components/CurrentAddress/index.js new file mode 100644 index 00000000..977029d5 --- /dev/null +++ b/src/components/CurrentAddress/index.js @@ -0,0 +1,206 @@ +// @flow + +import React, { PureComponent } from 'react' +import { translate } from 'react-i18next' +import styled from 'styled-components' + +import noop from 'lodash/noop' + +import type { T } from 'types/common' + +import { rgba } from 'styles/helpers' + +import Box from 'components/base/Box' +import CopyToClipboard from 'components/base/CopyToClipboard' +import Print from 'components/base/Print' +import QRCode from 'components/base/QRCode' + +import IconCheck from 'icons/Check' +import IconCopy from 'icons/Copy' +import IconInfoCircle from 'icons/InfoCircle' +import IconPrint from 'icons/Print' +import IconShare from 'icons/Share' +import IconShield from 'icons/Shield' + +const Container = styled(Box).attrs({ + borderRadius: 1, + alignItems: 'center', + bg: p => (p.withQRCode ? 'lightGrey' : 'transparent'), + py: 5, + px: 7, +})`` + +const WrapperAddress = styled(Box).attrs({ + alignItems: 'center', + borderRadius: 1, + py: p => (p.notValid ? 4 : 0), + px: 4, +})` + background: ${p => (p.notValid ? rgba(p.theme.colors.alertRed, 0.05) : 'transparent')}; + border: ${p => (p.notValid ? `1px dashed ${rgba(p.theme.colors.alertRed, 0.26)}` : 'none')}; + width: 100%; +` + +const Address = styled(Box).attrs({ + bg: p => (p.notValid ? 'transparent' : p.withQRCode ? 'white' : 'lightGrey'), + borderRadius: 1, + color: 'dark', + ff: 'Open Sans|SemiBold', + fontSize: 4, + mt: 2, + px: p => (p.notValid ? 0 : 4), + py: p => (p.notValid ? 0 : 3), +})` + border: ${p => (p.notValid ? 'none' : `1px dashed ${p.theme.colors.fog}`)}; + cursor: text; + user-select: text; +` + +const Label = styled(Box).attrs({ + alignItems: 'center', + color: 'graphite', + ff: 'Open Sans|SemiBold', + fontSize: 4, + flow: 1, + horizontal: true, +})`` + +const Footer = styled(Box).attrs({ + horizontal: true, + mt: 5, +})` + text-transform: uppercase; + width: 100%; +` + +const FooterButtonWrapper = styled(Box).attrs({ + color: 'grey', + alignItems: 'center', + justifyContent: 'center', + grow: true, +})` + cursor: pointer; +` + +const FooterButton = ({ + icon, + label, + onClick, +}: { + icon: any, + label: string, + onClick: Function, +}) => ( + + {icon} + + {label} + + +) + +type Props = { + address: string, + addressVerified?: boolean, + amount?: string, + onCopy: Function, + onPrint: Function, + onShare: Function, + onVerify: Function, + t: T, + withBadge: boolean, + withFooter: boolean, + withQRCode: boolean, + withVerify: boolean, +} + +class CurrentAddress extends PureComponent { + static defaultProps = { + addressVerified: null, + amount: null, + onCopy: noop, + onPrint: noop, + onShare: noop, + onVerify: noop, + withBadge: false, + withFooter: false, + withQRCode: false, + withVerify: false, + } + + render() { + const { + address, + addressVerified, + amount, + onCopy, + onPrint, + onShare, + onVerify, + t, + withBadge, + withFooter, + withQRCode, + withVerify, + ...props + } = this.props + + const notValid = addressVerified === false + + return ( + + + {withQRCode && ( + + + + )} + +
+ {address} +
+
+ {withBadge && ( + + + + + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam blandit velit egestas leo + tincidunt + + + )} + {withFooter && ( +
+ {withVerify && ( + } label="Verify" onClick={onVerify} /> + )} + ( + } label="Copy" onClick={copy} /> + )} + /> + ( + } + label={isLoading ? '...' : 'Print'} + onClick={print} + /> + )} + /> + } label="Share" onClick={onShare} /> +
+ )} +
+ ) + } +} + +export default translate()(CurrentAddress) diff --git a/src/components/CurrentAddress/stories.js b/src/components/CurrentAddress/stories.js new file mode 100644 index 00000000..188b4c00 --- /dev/null +++ b/src/components/CurrentAddress/stories.js @@ -0,0 +1,22 @@ +// @flow + +import React from 'react' +import { storiesOf } from '@storybook/react' +import { boolean } from '@storybook/addon-knobs' + +import CurrentAddress from 'components/CurrentAddress' + +import { accounts } from 'components/SelectAccount/stories' + +const stories = storiesOf('Components', module) + +stories.add('CurrentAddress', () => ( + +)) diff --git a/src/components/DeviceCheckAddress.js b/src/components/DeviceCheckAddress.js new file mode 100644 index 00000000..1d9beff5 --- /dev/null +++ b/src/components/DeviceCheckAddress.js @@ -0,0 +1,69 @@ +// @flow + +import { PureComponent } from 'react' +import { ipcRenderer } from 'electron' + +import type { Account } from '@ledgerhq/wallet-common/lib/types' +import type { Device } from 'types/common' + +import { sendEvent } from 'renderer/events' + +type Props = { + onCheck: Function, + render: Function, + account: Account, + device: Device, +} + +type State = { + isVerified: null | boolean, +} + +class CheckAddress extends PureComponent { + state = { + isVerified: null, + } + + componentDidMount() { + const { device, account } = this.props + ipcRenderer.on('msg', this.handleMsgEvent) + this.verifyAddress({ device, account }) + } + + componentWillUnmount() { + ipcRenderer.removeListener('msg', this.handleMsgEvent) + } + + handleMsgEvent = (e: any, { type }: { type: string }) => { + const { onCheck } = this.props + + if (type === 'wallet.verifyAddress.success') { + this.setState({ + isVerified: true, + }) + onCheck(true) + } + + if (type === 'wallet.verifyAddress.fail') { + this.setState({ + isVerified: false, + }) + onCheck(false) + } + } + + verifyAddress = ({ device, account }: { device: Device, account: Account }) => + sendEvent('usb', 'wallet.verifyAddress', { + pathDevice: device.path, + path: `${account.rootPath}${account.path}`, + }) + + render() { + const { render } = this.props + const { isVerified } = this.state + + return render({ isVerified }) + } +} + +export default CheckAddress diff --git a/src/components/DeviceConfirm/index.js b/src/components/DeviceConfirm/index.js new file mode 100644 index 00000000..a4c5e94e --- /dev/null +++ b/src/components/DeviceConfirm/index.js @@ -0,0 +1,178 @@ +// @flow + +import React from 'react' +import styled, { keyframes } from 'styled-components' + +import { rgba } from 'styles/helpers' + +import Box from 'components/base/Box' + +import IconCheck from 'icons/Check' +import IconCross from 'icons/Cross' + +const pulseAnimation = p => keyframes` + 0% { + box-shadow: 0 0 0 1px ${rgba(p.theme.colors.wallet, 0.4)}; + } + 70% { + box-shadow: 0 0 0 6px ${rgba(p.theme.colors.wallet, 0)}; + } + 100% { + box-shadow: 0 0 0 0 ${rgba(p.theme.colors.wallet, 0)}; + } +` + +const Wrapper = styled(Box).attrs({ + color: p => (p.notValid ? 'alertRed' : 'wallet'), + relative: true, +})` + padding-top: ${p => (p.notValid ? 0 : 30)}px; + transition: color ease-in-out 0.1s; +` + +const WrapperIcon = styled(Box)` + color: ${p => (p.notValid ? p.theme.colors.alertRed : p.theme.colors.positiveGreen)}; + position: absolute; + left: ${p => (p.notValid ? 152 : 193)}px; + bottom: 16px; + + svg { + transition: color ease-in-out 0.1s; + } +` + +const Check = ({ notValid }: { notValid: boolean }) => ( + + {notValid ? : } + +) + +const PushButton = styled(Box)` + background: linear-gradient(to bottom, #ffffff, ${p => p.theme.colors.wallet}); + bottom: 48px; + height: 28px; + left: 205px; + position: absolute; + width: 1px; + + &:before { + animation: ${p => pulseAnimation(p)} 1s linear infinite; + background-color: ${p => p.theme.colors.wallet}; + border-radius: 50%; + bottom: 0; + box-sizing: border-box; + content: ' '; + display: block; + height: 9px; + left: 50%; + margin-bottom: -4px; + margin-left: -4px; + position: absolute; + width: 9px; + z-index: 1; + } +` + +type Props = { + notValid: boolean, +} + +const SVG = ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + +) + +const DeviceConfirm = (props: Props) => ( + + {!props.notValid && } + + {SVG} + +) + +DeviceConfirm.defaultProps = { + notValid: false, +} + +export default DeviceConfirm diff --git a/src/components/DeviceConfirm/stories.js b/src/components/DeviceConfirm/stories.js new file mode 100644 index 00000000..60d53d65 --- /dev/null +++ b/src/components/DeviceConfirm/stories.js @@ -0,0 +1,11 @@ +// @flow + +import React from 'react' +import { storiesOf } from '@storybook/react' +import { boolean } from '@storybook/addon-knobs' + +import DeviceConfirm from 'components/DeviceConfirm' + +const stories = storiesOf('Components', module) + +stories.add('DeviceConfirm', () => ) diff --git a/src/components/DeviceMonitNew/index.js b/src/components/DeviceMonitNew/index.js index 7d7f8b93..193b69d0 100644 --- a/src/components/DeviceMonitNew/index.js +++ b/src/components/DeviceMonitNew/index.js @@ -140,7 +140,7 @@ class DeviceMonit extends PureComponent { if (render) { return render({ appStatus, - coinType: (account && account.coinType) || coinType, + coinType: account ? account.coinType : coinType, devices, deviceSelected: deviceStatus === 'connected' ? deviceSelected : null, deviceStatus, diff --git a/src/components/RequestAmount/index.js b/src/components/RequestAmount/index.js index d8dcd796..b037f75f 100644 --- a/src/components/RequestAmount/index.js +++ b/src/components/RequestAmount/index.js @@ -64,9 +64,17 @@ type Props = { // used to calculate the opposite field value (right & left) getCounterValue: CalculateCounterValue, getReverseCounterValue: CalculateCounterValue, + + // display max button + withMax: boolean, } export class RequestAmount extends PureComponent { + static defaultProps = { + max: Infinity, + withMax: true, + } + handleClickMax = () => { this.props.onChange(this.props.max) } @@ -81,35 +89,49 @@ export class RequestAmount extends PureComponent { } } - render() { - const { t, value, account, rightUnit, getCounterValue } = this.props + renderInputs(containerProps: Object) { + const { value, account, rightUnit, getCounterValue } = this.props const right = getCounterValue(account.currency, rightUnit)(value) return ( - - - {account.unit.code}} - /> - = - {rightUnit.code}} - showAllDigits - /> - - - - + + {account.unit.code}} + /> + = + {rightUnit.code}} + showAllDigits + /> + + ) + } + + render() { + const { withMax, t } = this.props + + return ( + + {withMax ? ( + {this.renderInputs({ style: { width: 156 } })} + ) : ( + this.renderInputs({ grow: true }) + )} + {withMax && ( + + + + )} ) } diff --git a/src/components/RequestAmount/stories.js b/src/components/RequestAmount/stories.js index d4aa88a4..049968ab 100644 --- a/src/components/RequestAmount/stories.js +++ b/src/components/RequestAmount/stories.js @@ -2,6 +2,7 @@ import React, { PureComponent } from 'react' import { storiesOf } from '@storybook/react' +import { text, boolean } from '@storybook/addon-knobs' import { action } from '@storybook/addon-actions' import { accounts } from 'components/SelectAccount/stories' @@ -23,17 +24,21 @@ class Wrapper extends PureComponent { this.setState({ value }) } render() { + const { max, withMax } = this.props const { value } = this.state return ( ) } } -stories.add('RequestAmount', () => ) +stories.add('RequestAmount', () => ( + +)) diff --git a/src/components/SelectAccount/index.js b/src/components/SelectAccount/index.js index cc1f322f..2d098ecd 100644 --- a/src/components/SelectAccount/index.js +++ b/src/components/SelectAccount/index.js @@ -24,7 +24,7 @@ const renderItem = a => { const Icon = getIconByCoinType(a.coinType) const { color } = a.currency return ( - + {Icon && ( @@ -49,8 +49,9 @@ type Props = { t: T, } -const RawSelectAccount = ({ accounts, onChange, value, t }: Props) => ( +const RawSelectAccount = ({ accounts, onChange, value, t, ...props }: Props) => ( - - - + + } + /> ) : ( - {selectedItem && renderSelected ? ( - renderSelected(selectedItem) - ) : ( - {placeholder} - )} + {renderSelectedItem({ selectedItem, renderSelected, placeholder })} - - - + )} {isOpen && diff --git a/src/components/base/Select/stories.js b/src/components/base/Select/stories.js index ba750dd5..75b0c414 100644 --- a/src/components/base/Select/stories.js +++ b/src/components/base/Select/stories.js @@ -2,6 +2,7 @@ import React, { PureComponent } from 'react' import { storiesOf } from '@storybook/react' +import { boolean } from '@storybook/addon-knobs' import Box from 'components/base/Box' import Select from 'components/base/Select' @@ -57,6 +58,7 @@ stories.add('basic', () => ( {onChange => (