From 6f9ff91ad6d9add9a6bef31f1f43c187eae8e49d Mon Sep 17 00:00:00 2001 From: Tom Kirkpatrick Date: Thu, 1 Nov 2018 23:56:13 +0100 Subject: [PATCH 1/6] feat(ui): Add Form component Component that wraps Informed.Form as a Styled System component. --- app/components/UI/Form.js | 12 ++++++++++++ app/components/UI/index.js | 1 + test/unit/components/UI/Form.spec.js | 10 ++++++++++ .../components/UI/__snapshots__/Form.spec.js.snap | 9 +++++++++ 4 files changed, 32 insertions(+) create mode 100644 app/components/UI/Form.js create mode 100644 test/unit/components/UI/Form.spec.js create mode 100644 test/unit/components/UI/__snapshots__/Form.spec.js.snap diff --git a/app/components/UI/Form.js b/app/components/UI/Form.js new file mode 100644 index 00000000..9a9355c1 --- /dev/null +++ b/app/components/UI/Form.js @@ -0,0 +1,12 @@ +import system from '@rebass/components' +import { styles } from 'styled-system' +import { Form as InformedForm } from 'informed' +// Create an html input element that accepts all style props from styled-system. +const Form = system( + { + extend: InformedForm + }, + ...Object.keys(styles) +) + +export default Form diff --git a/app/components/UI/index.js b/app/components/UI/index.js index 75fd429c..400055fa 100644 --- a/app/components/UI/index.js +++ b/app/components/UI/index.js @@ -6,6 +6,7 @@ export Button from './Button' export CryptoAmountInput from './CryptoAmountInput' export Dropdown from './Dropdown' export FiatAmountInput from './FiatAmountInput' +export Form from './Form' export FormFieldMessage from './FormFieldMessage' export GlobalStyle from './GlobalStyle' export Heading from './Heading' diff --git a/test/unit/components/UI/Form.spec.js b/test/unit/components/UI/Form.spec.js new file mode 100644 index 00000000..e4cb93eb --- /dev/null +++ b/test/unit/components/UI/Form.spec.js @@ -0,0 +1,10 @@ +import React from 'react' +import renderer from 'react-test-renderer' +import { Form } from 'components/UI' + +describe('component.UI.Form', () => { + it('should render correctly', () => { + const tree = renderer.create(
).toJSON() + expect(tree).toMatchSnapshot() + }) +}) diff --git a/test/unit/components/UI/__snapshots__/Form.spec.js.snap b/test/unit/components/UI/__snapshots__/Form.spec.js.snap new file mode 100644 index 00000000..cb18cd9d --- /dev/null +++ b/test/unit/components/UI/__snapshots__/Form.spec.js.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`component.UI.Form should render correctly 1`] = ` + +`; From 2d9493b9d47900a2b764a0111b308269d0019319 Mon Sep 17 00:00:00 2001 From: Tom Kirkpatrick Date: Fri, 2 Nov 2018 00:01:42 +0100 Subject: [PATCH 2/6] feat(ui): Add Truncate component Component that truncates text to a given length with an ellipsis in the middle of the string. --- app/components/UI/Truncate.js | 21 +++++++++++++++++++ app/components/UI/index.js | 1 + test/unit/components/UI/Truncate.spec.js | 21 +++++++++++++++++++ .../UI/__snapshots__/Truncate.spec.js.snap | 5 +++++ 4 files changed, 48 insertions(+) create mode 100644 app/components/UI/Truncate.js create mode 100644 test/unit/components/UI/Truncate.spec.js create mode 100644 test/unit/components/UI/__snapshots__/Truncate.spec.js.snap diff --git a/app/components/UI/Truncate.js b/app/components/UI/Truncate.js new file mode 100644 index 00000000..645d8615 --- /dev/null +++ b/app/components/UI/Truncate.js @@ -0,0 +1,21 @@ +import PropTypes from 'prop-types' + +const Truncate = ({ text, maxlen = 12 }) => { + if (!text) { + return null + } + + const truncatedText = + text.length < maxlen + ? text + : text.substr(0, maxlen / 2) + '...' + text.substr(text.length - maxlen / 2) + + return truncatedText +} + +Truncate.propTypes = { + text: PropTypes.string.isRequired, + maxlen: PropTypes.number +} + +export default Truncate diff --git a/app/components/UI/index.js b/app/components/UI/index.js index 400055fa..8b435b8f 100644 --- a/app/components/UI/index.js +++ b/app/components/UI/index.js @@ -28,3 +28,4 @@ export Text from './Text' export TextArea from './TextArea' export Titlebar from './Titlebar' export Toggle from './Toggle' +export Truncate from './Truncate' diff --git a/test/unit/components/UI/Truncate.spec.js b/test/unit/components/UI/Truncate.spec.js new file mode 100644 index 00000000..95dcc712 --- /dev/null +++ b/test/unit/components/UI/Truncate.spec.js @@ -0,0 +1,21 @@ +import React from 'react' +import renderer from 'react-test-renderer' +import { Truncate } from 'components/UI' + +describe('component.UI.Truncate', () => { + it('should truncate text to 12 chars by default', () => { + const tree = renderer + .create() + .toJSON() + expect(tree).toMatchSnapshot() + }) + + it('should truncate test to a specific length when the maxlen parm is provided', () => { + const tree = renderer + .create( + + ) + .toJSON() + expect(tree).toMatchSnapshot() + }) +}) diff --git a/test/unit/components/UI/__snapshots__/Truncate.spec.js.snap b/test/unit/components/UI/__snapshots__/Truncate.spec.js.snap new file mode 100644 index 00000000..c8e13f62 --- /dev/null +++ b/test/unit/components/UI/__snapshots__/Truncate.spec.js.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`component.UI.Truncate should truncate test to a specific length when the maxlen parm is provided 1`] = `"Lorem ipsum dol...adipiscing elit"`; + +exports[`component.UI.Truncate should truncate text to 12 chars by default 1`] = `"Lorem ...g elit"`; From b18d08b70e7540fe71d4a6afe54482cecb202726 Mon Sep 17 00:00:00 2001 From: Tom Kirkpatrick Date: Fri, 2 Nov 2018 00:04:26 +0100 Subject: [PATCH 3/6] feat(crypto): add utility methods for lnd data Adds a few basic utility methods for dealing with lnd related data. --- app/lib/utils/crypto.js | 58 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/app/lib/utils/crypto.js b/app/lib/utils/crypto.js index ba7c35e4..39b3a74d 100644 --- a/app/lib/utils/crypto.js +++ b/app/lib/utils/crypto.js @@ -59,6 +59,10 @@ export const parseNumber = (_value, precision) => { * @return {Boolean} boolean indicating wether the address is a valid on-chain address. */ export const isOnchain = (input, chain = 'bitcoin', network = 'mainnet') => { + if (typeof input !== 'string') { + return false + } + if (chain !== 'bitcoin') { // TODO: Implement address checking for litecoin. return true @@ -82,6 +86,10 @@ export const isOnchain = (input, chain = 'bitcoin', network = 'mainnet') => { * @return {Boolean} boolean indicating wether the address is a lightning address. */ export const isLn = (input, chain = 'bitcoin', network = 'mainnet') => { + if (typeof input !== 'string') { + return false + } + let prefix = 'ln' // Prefixes come from SLIP-0173 // See https://github.com/satoshilabs/slips/blob/master/slip-0173.md @@ -122,3 +130,53 @@ export const isLn = (input, chain = 'bitcoin', network = 'mainnet') => { return false } } + +/** + * Get a nodes alias. + * @param {String} pubkey pubKey of node to fetch alias for. + * @param {Array} Node list to search. + * @return {String} Node alias, if found + */ +export const getNodeAlias = (pubkey, nodes = []) => { + const node = nodes.find(n => n.pub_key === pubkey) + + if (node && node.alias.length) { + return node.alias + } + + return null +} + +/** + * Given a list of routest, find the minimum fee. + * @param {QueryRoutesResponse} routes + * @return {Number} minimum fee. + */ +export const getMinFee = (routes = []) => { + if (!routes || !routes.length) { + return null + } + return routes.reduce((min, b) => Math.min(min, b.total_fees_msat), routes[0].total_fees_msat) +} + +/** + * Given a list of routest, find the maximum fee. + * @param {QueryRoutesResponse} routes + * @return {Number} maximum fee. + */ +export const getMaxFee = routes => { + if (!routes || !routes.length) { + return null + } + return routes.reduce((max, b) => Math.max(max, b.total_fees_msat), routes[0].total_fees_msat) +} + +/** + * Given a list of routest, find the maximum and maximum fee. + * @param {QueryRoutesResponse} routes + * @return {Object} object with kets `min` and `max` + */ +export const getFeeRange = (routes = []) => ({ + min: getMinFee(routes), + max: getMaxFee(routes) +}) From b850cd8c80f72e71174b030443fd7f72084a0cc0 Mon Sep 17 00:00:00 2001 From: Tom Kirkpatrick Date: Fri, 2 Nov 2018 00:27:52 +0100 Subject: [PATCH 4/6] feat(i18n): multilingual support for UI components --- app/components/UI/LightningInvoiceInput.js | 32 +++++++++++++++---- app/components/UI/messages.js | 9 ++++++ .../UI/LightningInvoiceInput.spec.js | 13 +++++--- 3 files changed, 43 insertions(+), 11 deletions(-) create mode 100644 app/components/UI/messages.js diff --git a/app/components/UI/LightningInvoiceInput.js b/app/components/UI/LightningInvoiceInput.js index a5057ff1..3ef8fe30 100644 --- a/app/components/UI/LightningInvoiceInput.js +++ b/app/components/UI/LightningInvoiceInput.js @@ -1,9 +1,11 @@ import React from 'react' import PropTypes from 'prop-types' +import { FormattedMessage, injectIntl } from 'react-intl' import { asField } from 'informed' import { isOnchain, isLn } from 'lib/utils/crypto' import TextArea from 'components/UI/TextArea' import FormFieldMessage from 'components/UI/FormFieldMessage' +import messages from './messages' /** * @render react @@ -30,19 +32,28 @@ class LightningInvoiceInput extends React.Component { } validate = value => { + const { intl } = this.props const { network, chain, required } = this.props + + let chainName = `${chain}/lightning` + if (network !== 'mainnet') { + chainName += ` (${network})` + } + if (required && (!value || value.trim() === '')) { - return 'This is a required field' + return intl.formatMessage({ ...messages.required_field }) } if (value && !isLn(value, chain, network) && !isOnchain(value, chain, network)) { - return 'Not a valid address.' + return intl.formatMessage({ ...messages.invalid_request }, { chain: chainName }) } } render() { + const { intl } = this.props + return ( { const { value } = fieldState const { chain, network, ...rest } = props + + let chainName = isLn(value, chain, network) ? 'lightning' : chain + if (network !== 'mainnet') { + chainName += ` (${network})` + } return (