diff --git a/app/components/Pay/Pay.js b/app/components/Pay/Pay.js
new file mode 100644
index 00000000..90f6e87c
--- /dev/null
+++ b/app/components/Pay/Pay.js
@@ -0,0 +1,702 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { Box, Flex } from 'rebass'
+import { animated, Keyframes, Transition } from 'react-spring'
+import { FormattedMessage, injectIntl } from 'react-intl'
+import lightningPayReq from 'bolt11'
+import { getMinFee, getMaxFee, getFeeRange, isOnchain, isLn } from 'lib/utils/crypto'
+import { convert } from 'lib/utils/btc'
+import {
+ Bar,
+ CryptoAmountInput,
+ Dropdown,
+ FiatAmountInput,
+ Form,
+ FormFieldMessage,
+ Label,
+ LightningInvoiceInput,
+ Text
+} from 'components/UI'
+
+import PayButtons from './PayButtons'
+import PayHeader from './PayHeader'
+import { PaySummaryLightning, PaySummaryOnChain } from '.'
+import messages from './messages'
+
+/**
+ * Animation to handle showing/hiding the payReq field.
+ */
+const ShowHidePayReq = Keyframes.Spring({
+ small: { height: 48 },
+ big: async (next, cancel, ownProps) => {
+ ownProps.context.focusPayReqInput()
+ await next({ height: 130 })
+ }
+})
+
+/**
+ * Animation to handle showing/hiding the form buttons.
+ */
+const ShowHideButtons = Keyframes.Spring({
+ show: { opacity: 1 },
+ hide: { opacity: 0 }
+})
+
+/**
+ * Animation to handle showing/hiding the amount fields.
+ */
+const ShowHideAmount = Keyframes.Spring({
+ show: async (next, cancel, ownProps) => {
+ await next({ display: 'block' })
+ ownProps.context.focusAmountInput()
+ await next({ opacity: 1, height: 'auto' })
+ },
+ hide: { opacity: 0, height: 0, display: 'none' },
+ remove: { opacity: 0, height: 0, display: 'none', immediate: true }
+})
+
+/**
+ * Payment form (onchain & offchain)
+ */
+class Pay extends React.Component {
+ static propTypes = {
+ /** The currently active chain (bitcoin, litecoin etc) */
+ chain: PropTypes.string.isRequired,
+ /** The currently active chain (mainnet, testnet) */
+ network: PropTypes.string.isRequired,
+ /** Human readable chain name */
+ cryptoName: PropTypes.string.isRequired,
+ /** Current channel balance (in satoshis). */
+ channelBalance: PropTypes.number.isRequired,
+ /** Current ticker data as provided by blockchain.info */
+ currentTicker: PropTypes.object.isRequired,
+ /** Currently selected cryptocurrency (key). */
+ cryptoCurrency: PropTypes.string.isRequired,
+ /** Ticker symbol of the currently selected cryptocurrency. */
+ cryptoCurrencyTicker: PropTypes.string.isRequired,
+ /** List of supported cryptocurrencies. */
+ cryptoCurrencies: PropTypes.arrayOf(
+ PropTypes.shape({
+ key: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired
+ })
+ ).isRequired,
+ /** List of supported fiat currencies. */
+ fiatCurrencies: PropTypes.array.isRequired,
+ /** Currently selected fiat currency (key). */
+ fiatCurrency: PropTypes.string.isRequired,
+ /** Payment address or invoice to populate the payReq field with when the form first loads. */
+ initialPayReq: PropTypes.string,
+ /** Amount value to populate the amountCrypto field with when the form first loads. */
+ initialAmountCrypto: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ /** Amount value to populate the amountFiat field with when the form first loads. */
+ initialAmountFiat: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ /** Boolean indicating wether the form is being processed. If true, form buttons are disabled. */
+ isProcessing: PropTypes.bool,
+ /** Boolean indicating wether fee information is currently being fetched. */
+ isQueryingFees: PropTypes.bool,
+ /** Boolean indicating wether routing information is currently being fetched. */
+ isQueryingRoutes: PropTypes.bool,
+ /** List of known nodes */
+ nodes: PropTypes.array,
+ /** Current fee information as provided by bitcoinfees.earn.com */
+ onchainFees: PropTypes.shape({
+ fastestFee: PropTypes.number,
+ halfHourFee: PropTypes.number,
+ hourFee: PropTypes.number
+ }),
+ /** Routing information */
+ routes: PropTypes.array,
+ /** Current wallet balance (in satoshis). */
+ walletBalance: PropTypes.number.isRequired,
+ /** Method to process offChain invoice payments. Called when the form is submitted. */
+ payInvoice: PropTypes.func.isRequired,
+ /** Set the current cryptocurrency. */
+ setCryptoCurrency: PropTypes.func.isRequired,
+ /** Set the current fiat currency */
+ setFiatCurrency: PropTypes.func.isRequired,
+ /** Method to process onChain transactions. Called when the form is submitted. */
+ sendCoins: PropTypes.func.isRequired,
+ /** Method to fetch fee information for onchain transactions. */
+ queryFees: PropTypes.func.isRequired,
+ /** Method to collect route information for lightning invoices. */
+ queryRoutes: PropTypes.func.isRequired
+ }
+
+ static defaultProps = {
+ initialAmountCrypto: null,
+ initialAmountFiat: null,
+ isProcessing: false,
+ isQueryingFees: false,
+ isQueryingRoutes: false,
+ nodes: [],
+ routes: []
+ }
+
+ state = {
+ currentStep: 'address',
+ isLn: null,
+ isOnchain: null
+ }
+
+ amountInput = React.createRef()
+ payReqInput = React.createRef()
+
+ /**
+ * If we have an address when the component mounts, run the payReq change handler to compure isLn / isOnchain.
+ */
+ componentDidMount() {
+ const { formApi } = this
+ if (formApi.getValue('payReq')) {
+ this.handlePayReqOnChange()
+ }
+ }
+
+ /**
+ * If we have gone back to the address step, focus the address input and unmark all fields from being touched.
+ */
+ componentDidUpdate(prevProps, prevState) {
+ const { currentStep } = this.state
+ if (currentStep !== prevState.currentStep) {
+ if (currentStep === 'address') {
+ Object.keys(this.formApi.getState().touched).forEach(field => {
+ this.formApi.setTouched(field, false)
+ })
+ }
+ }
+ }
+
+ /**
+ * Form submit handler.
+ * @param {Object} values submitted form values.
+ */
+ onSubmit = values => {
+ const { currentStep, isOnchain } = this.state
+ const { cryptoCurrency, payInvoice, sendCoins } = this.props
+ if (currentStep === 'summary') {
+ return isOnchain
+ ? sendCoins({
+ value: values.amountCrypto,
+ addr: values.payReq,
+ currency: cryptoCurrency
+ })
+ : payInvoice(values.payReq)
+ } else {
+ this.nextStep()
+ }
+ }
+
+ /**
+ * Store the formApi on the component context to make it available at this.formApi.
+ */
+ setFormApi = formApi => {
+ this.formApi = formApi
+ }
+
+ /**
+ * set the amountFiat field.
+ */
+ setAmountFiat = () => {
+ if (this.amountInput.current) {
+ this.amountInput.current.focus()
+ }
+ }
+
+ /**
+ * Focus the payReq input.
+ */
+ focusPayReqInput = () => {
+ if (this.payReqInput.current) {
+ this.payReqInput.current.focus()
+ }
+ }
+
+ /**
+ * Focus the amount input.
+ */
+ focusAmountInput = () => {
+ if (this.amountInput.current) {
+ this.amountInput.current.focus()
+ }
+ }
+
+ /**
+ * Liost of enabled form steps.
+ */
+ steps = () => {
+ const { isLn, isOnchain } = this.state
+ let steps = ['address']
+ if (isLn) {
+ steps = ['address', 'summary']
+ } else if (isOnchain) {
+ steps = ['address', 'amount', 'summary']
+ }
+ return steps
+ }
+
+ /**
+ * Go back to previous form step.
+ */
+ previousStep = () => {
+ const { currentStep } = this.state
+ const nextStep = Math.max(this.steps().indexOf(currentStep) - 1, 0)
+ if (currentStep !== nextStep) {
+ this.setState({ currentStep: this.steps()[nextStep] })
+ }
+ }
+
+ /**
+ * Progress to next form step.
+ */
+ nextStep = () => {
+ const { currentStep } = this.state
+ const nextStep = Math.min(this.steps().indexOf(currentStep) + 1, this.steps().length - 1)
+ if (currentStep !== nextStep) {
+ this.setState({ currentStep: this.steps()[nextStep] })
+ }
+ }
+
+ /**
+ * Set isLn/isOnchain state based on payReq value.
+ */
+ handlePayReqOnChange = () => {
+ const { chain, network, queryRoutes } = this.props
+ const payReq = this.formApi.getValue('payReq')
+ const state = {
+ isLn: null,
+ isOnchain: null,
+ invoice: null
+ }
+
+ // See if the user has entered a valid lightning payment request.
+ if (isLn(payReq, chain, network)) {
+ let invoice
+ try {
+ invoice = lightningPayReq.decode(payReq)
+ state.invoice = invoice
+ } catch (e) {
+ return
+ }
+ state.isLn = true
+ const { satoshis, payeeNodeKey } = invoice
+ queryRoutes(payeeNodeKey, satoshis)
+ }
+
+ // Otherwise, see if we have a valid onchain address.
+ else if (isOnchain(payReq, chain, network)) {
+ state.isOnchain = true
+ }
+
+ // Update the state with our findings.
+ this.setState(state)
+
+ // As soon as we have detected a valid address, submit the form.
+ if (state.isLn || state.isOnchain) {
+ this.formApi.submitForm()
+ }
+ }
+
+ /**
+ * set the amountFiat field whenever the crypto amount changes.
+ */
+ handleAmountCryptoChange = e => {
+ const { cryptoCurrency, currentTicker, fiatCurrency } = this.props
+ const lastPrice = currentTicker[fiatCurrency].last
+ const value = convert(cryptoCurrency, 'fiat', e.target.value, lastPrice)
+ this.formApi.setValue('amountFiat', value)
+ }
+
+ /**
+ * set the amountCrypto field whenever the fiat amount changes.
+ */
+ handleAmountFiatChange = e => {
+ const { cryptoCurrency, currentTicker, fiatCurrency } = this.props
+ const lastPrice = currentTicker[fiatCurrency].last
+ const value = convert('fiat', cryptoCurrency, e.target.value, lastPrice)
+ this.formApi.setValue('amountCrypto', value)
+ }
+
+ /**
+ * Handle changes from the crypto currency dropdown.
+ */
+ handleCryptoCurrencyChange = value => {
+ const { setCryptoCurrency } = this.props
+ setCryptoCurrency(value)
+ }
+
+ /**
+ * Handle changes from the fiat currency dropdown.
+ */
+ handleFiatCurrencyChange = value => {
+ const { setFiatCurrency } = this.props
+ setFiatCurrency(value)
+ }
+
+ renderHelpText = () => {
+ const { currentStep } = this.state
+ return (
+
+ {show =>
+ show &&
+ (styles => (
+
+
+
+
+
+
+
+ ))
+ }
+
+ )
+ }
+
+ renderAddressField = () => {
+ const { currentStep, isLn } = this.state
+ const { chain, initialPayReq, network } = this.props
+ return (
+
+
+
+
+
+
+ {styles => (
+
+
+
+ )}
+
+
+ )
+ }
+
+ renderAmountFields = () => {
+ const { currentStep } = this.state
+ const {
+ cryptoCurrency,
+ cryptoCurrencies,
+ currentTicker,
+ fiatCurrency,
+ fiatCurrencies,
+ initialAmountCrypto,
+ initialAmountFiat
+ } = this.props
+ return (
+
+ {styles => (
+
+
+
+
+
+
+
+
+
+
+
+
+ =
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ )
+ }
+
+ renderSummary = () => {
+ const { isOnchain } = this.state
+ const {
+ cryptoCurrency,
+ cryptoCurrencyTicker,
+ cryptoCurrencies,
+ currentTicker,
+ fiatCurrency,
+ isQueryingFees,
+ isQueryingRoutes,
+ nodes,
+ onchainFees,
+ queryFees,
+ routes,
+ setCryptoCurrency
+ } = this.props
+ const formState = this.formApi.getState()
+ let minFee, maxFee
+ if (routes.length) {
+ minFee = getMinFee(routes)
+ maxFee = getMaxFee(routes)
+ }
+
+ // convert entered amount to satoshis
+ if (isOnchain) {
+ const amountInSatoshis = convert(cryptoCurrency, 'sats', formState.values.amountCrypto)
+ return (
+
+ )
+ } else if (isLn) {
+ return (
+
+ )
+ }
+ }
+
+ /**
+ * Form renderer.
+ */
+ render() {
+ const { currentStep, invoice, isLn, isOnchain } = this.state
+ const {
+ chain,
+ network,
+ channelBalance,
+ cryptoCurrency,
+ cryptoCurrencyTicker,
+ cryptoCurrencies,
+ currentTicker,
+ cryptoName,
+ fiatCurrencies,
+ fiatCurrency,
+ initialPayReq,
+ initialAmountCrypto,
+ initialAmountFiat,
+ intl,
+ isProcessing,
+ isQueryingFees,
+ isQueryingRoutes,
+ onchainFees,
+ payInvoice,
+ sendCoins,
+ setCryptoCurrency,
+ setFiatCurrency,
+ queryFees,
+ queryRoutes,
+ routes,
+ walletBalance,
+ ...rest
+ } = this.props
+ return (
+
+ )
+ }
+}
+
+export default injectIntl(Pay)
diff --git a/app/components/Pay/PayButtons.js b/app/components/Pay/PayButtons.js
new file mode 100644
index 00000000..cf66ca0e
--- /dev/null
+++ b/app/components/Pay/PayButtons.js
@@ -0,0 +1,74 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { FormattedMessage } from 'react-intl'
+import { Box, Flex, Text } from 'rebass'
+import BigArrowLeft from 'components/Icon/BigArrowLeft'
+import { Button } from 'components/UI'
+import messages from './messages'
+
+/**
+ * Buttons for Pay.
+ */
+class PayButtons extends React.PureComponent {
+ static propTypes = {
+ disabled: PropTypes.bool,
+ nextButtonText: PropTypes.node,
+ previousStep: PropTypes.func,
+ processing: PropTypes.bool,
+ showBack: PropTypes.bool,
+ showSubmit: PropTypes.bool
+ }
+
+ static defaultProps = {
+ disabled: false,
+ nextButtonText: ,
+ previousStep: () => ({}),
+ processing: false,
+ showBack: true,
+ showSubmit: true
+ }
+
+ render() {
+ const {
+ disabled,
+ nextButtonText,
+ previousStep,
+ processing,
+ showBack,
+ showSubmit,
+ ...rest
+ } = this.props
+ return (
+
+
+ {showBack && (
+
+ )}
+
+ {showSubmit && (
+
+ )}
+
+
+ )
+ }
+}
+
+export default PayButtons
diff --git a/app/components/Pay/PayHeader.js b/app/components/Pay/PayHeader.js
new file mode 100644
index 00000000..9ed768a0
--- /dev/null
+++ b/app/components/Pay/PayHeader.js
@@ -0,0 +1,37 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { Box } from 'rebass'
+import { Heading, Text } from 'components/UI'
+import Lightning from 'components/Icon/Lightning'
+import Onchain from 'components/Icon/Onchain'
+import PaperPlane from 'components/Icon/PaperPlane'
+
+/**
+ * Header for opayment form.
+ */
+class PayHeader extends React.PureComponent {
+ static propTypes = {
+ title: PropTypes.string.isRequired,
+ type: PropTypes.oneOf(['onchain', 'offchain'])
+ }
+
+ render() {
+ const { title, type } = this.props
+ return (
+
+
+ {type === 'offchain' && }
+ {type === 'onchain' && }
+ {!type && }
+
+ {title}
+
+
+ {type === 'onchain' && 'On-Chain Payment'} {type === 'offchain' && 'Lightning Payment'}
+
+
+ )
+ }
+}
+
+export default PayHeader
diff --git a/app/components/Pay/PaySummaryLightning.js b/app/components/Pay/PaySummaryLightning.js
new file mode 100644
index 00000000..77556042
--- /dev/null
+++ b/app/components/Pay/PaySummaryLightning.js
@@ -0,0 +1,163 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { Box, Flex } from 'rebass'
+import { FormattedNumber, FormattedMessage } from 'react-intl'
+import lightningPayReq from 'bolt11'
+import { satoshisToFiat } from 'lib/utils/btc'
+import { getNodeAlias } from 'lib/utils/crypto'
+import BigArrowRight from 'components/Icon/BigArrowRight'
+import { Bar, Dropdown, Spinner, Text, Truncate } from 'components/UI'
+import Value from 'components/Value'
+import { PaySummaryRow } from '.'
+import messages from './messages'
+
+class PaySummaryLightning extends React.PureComponent {
+ static propTypes = {
+ /** Current ticker data as provided by blockchain.info */
+ currentTicker: PropTypes.object.isRequired,
+ /** Currently selected cryptocurrency (key). */
+ cryptoCurrency: PropTypes.string.isRequired,
+ /** Ticker symbol of the currently selected cryptocurrency. */
+ cryptoCurrencyTicker: PropTypes.string.isRequired,
+ /** List of supported cryptocurrencies. */
+ cryptoCurrencies: PropTypes.arrayOf(
+ PropTypes.shape({
+ key: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired
+ })
+ ).isRequired,
+ /** Currently selected fiat currency (key). */
+ fiatCurrency: PropTypes.string.isRequired,
+ /** Boolean indicating wether routing information is currently being fetched. */
+ isQueryingRoutes: PropTypes.bool,
+ /** Maximum fee for the payment */
+ maxFee: PropTypes.number,
+ /** Minimumfee for the payment */
+ minFee: PropTypes.number,
+ /** List of nodes as returned by lnd */
+ nodes: PropTypes.array,
+ /** Lightning Payment request */
+ payReq: PropTypes.string.isRequired,
+
+ /** Set the current cryptocurrency. */
+ setCryptoCurrency: PropTypes.func.isRequired
+ }
+
+ static defaultProps = {
+ isQueryingRoutes: false,
+ minFee: null,
+ maxFee: null,
+ nodes: []
+ }
+
+ render() {
+ const {
+ cryptoCurrency,
+ cryptoCurrencyTicker,
+ cryptoCurrencies,
+ currentTicker,
+ fiatCurrency,
+ isQueryingRoutes,
+ maxFee,
+ minFee,
+ nodes,
+ payReq,
+ setCryptoCurrency
+ } = this.props
+
+ let invoice
+ try {
+ invoice = lightningPayReq.decode(payReq)
+ } catch (e) {
+ return null
+ }
+
+ const { satoshis, payeeNodeKey } = invoice
+ const descriptionTag = invoice.tags.find(tag => tag.tagName === 'description') || {}
+ const memo = descriptionTag.data
+ const fiatAmount = satoshisToFiat(satoshis, currentTicker[fiatCurrency].last)
+ const nodeAlias = getNodeAlias(payeeNodeKey, nodes)
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {'≈ '}
+
+
+
+
+
+
+
+
+
+
+ {}
+
+
+
+
+
+
+
+ }
+ right={
+ isQueryingRoutes ? (
+
+
+
+ …
+
+
+
+ ) : minFee === null || maxFee === null ? (
+
+ ) : (
+
+ )
+ }
+ />
+
+
+
+ }
+ right={
+
+ {cryptoCurrencyTicker}
+ {!isQueryingRoutes &&
+ maxFee && (
+
+ (+ {maxFee} msats)
+
+ )}
+
+ }
+ />
+
+
+
+ {memo && } right={memo} />}
+
+ )
+ }
+}
+
+export default PaySummaryLightning
diff --git a/app/components/Pay/PaySummaryOnChain.js b/app/components/Pay/PaySummaryOnChain.js
new file mode 100644
index 00000000..3b87fba8
--- /dev/null
+++ b/app/components/Pay/PaySummaryOnChain.js
@@ -0,0 +1,155 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { Box, Flex } from 'rebass'
+import { FormattedNumber, FormattedMessage } from 'react-intl'
+import get from 'lodash.get'
+import { satoshisToFiat } from 'lib/utils/btc'
+import BigArrowRight from 'components/Icon/BigArrowRight'
+import { Bar, Dropdown, Spinner, Text, Truncate } from 'components/UI'
+import Value from 'components/Value'
+import { PaySummaryRow } from '.'
+import messages from './messages'
+
+class PaySummaryOnChain extends React.Component {
+ static propTypes = {
+ /** Amount to send (in satoshis). */
+ amount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
+ /** Onchain address of recipient. */
+ address: PropTypes.string.isRequired,
+ /** Currently selected cryptocurrency (key). */
+ cryptoCurrency: PropTypes.string.isRequired,
+ /** Ticker symbol of the currently selected cryptocurrency. */
+ cryptoCurrencyTicker: PropTypes.string.isRequired,
+ /** List of supported cryptocurrencies. */
+ cryptoCurrencies: PropTypes.arrayOf(
+ PropTypes.shape({
+ key: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired
+ })
+ ).isRequired,
+ /** Current ticker data as provided by blockchain.info */
+ currentTicker: PropTypes.object.isRequired,
+ /** Current fee information as provided by bitcoinfees.earn.com */
+ onchainFees: PropTypes.shape({
+ fastestFee: PropTypes.number,
+ halfHourFee: PropTypes.number,
+ hourFee: PropTypes.number
+ }),
+ /** Currently selected fiat currency (key). */
+ fiatCurrency: PropTypes.string.isRequired,
+ /** Boolean indicating wether routing information is currently being fetched. */
+ isQueryingFees: PropTypes.bool,
+
+ /** Method to fetch fee information for onchain transactions. */
+ queryFees: PropTypes.func.isRequired,
+ /** Set the current cryptocurrency. */
+ setCryptoCurrency: PropTypes.func.isRequired
+ }
+
+ static defaultProps = {
+ isQueryingFees: false,
+ onchainFees: {}
+ }
+
+ componenDidMount() {
+ const { queryFees } = this.props
+ queryFees()
+ }
+
+ render() {
+ const {
+ amount,
+ address,
+ cryptoCurrency,
+ cryptoCurrencyTicker,
+ cryptoCurrencies,
+ currentTicker,
+ fiatCurrency,
+ onchainFees,
+ isQueryingFees,
+ setCryptoCurrency
+ } = this.props
+
+ const fiatAmount = satoshisToFiat(amount, currentTicker[fiatCurrency].last)
+ const fee = get(onchainFees, 'fastestFee', null)
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {' ≈ '}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ right={
+ isQueryingFees ? (
+
+
+
+ …
+
+
+
+ ) : !fee ? (
+
+ ) : (
+
+
+ {fee} satoshis
+
+
+ ()
+
+
+ )
+ }
+ />
+
+
+
+ }
+ right={
+
+ {cryptoCurrencyTicker}
+ {!isQueryingFees && fee && (+ {fee} satoshis per byte}
+
+ }
+ />
+
+ )
+ }
+}
+
+export default PaySummaryOnChain
diff --git a/app/components/Pay/PaySummaryRow.js b/app/components/Pay/PaySummaryRow.js
new file mode 100644
index 00000000..6e02b4b2
--- /dev/null
+++ b/app/components/Pay/PaySummaryRow.js
@@ -0,0 +1,24 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { Box, Flex } from 'rebass'
+import { Text } from 'components/UI'
+
+const PaySummaryRow = ({ left, right }) => (
+
+
+
+ {left}
+
+
+ {right}
+
+
+
+)
+
+PaySummaryRow.propTypes = {
+ left: PropTypes.any,
+ right: PropTypes.any
+}
+
+export default PaySummaryRow
diff --git a/app/components/Pay/index.js b/app/components/Pay/index.js
new file mode 100644
index 00000000..1a345aac
--- /dev/null
+++ b/app/components/Pay/index.js
@@ -0,0 +1,6 @@
+export Pay from './Pay'
+export PayButtons from './PayButtons'
+export PayHeader from './PayHeader'
+export PaySummaryLightning from './PaySummaryLightning'
+export PaySummaryOnChain from './PaySummaryOnChain'
+export PaySummaryRow from './PaySummaryRow'
diff --git a/app/components/Pay/messages.js b/app/components/Pay/messages.js
new file mode 100644
index 00000000..173a8134
--- /dev/null
+++ b/app/components/Pay/messages.js
@@ -0,0 +1,27 @@
+import { defineMessages } from 'react-intl'
+
+/* eslint-disable max-len */
+export default defineMessages({
+ calculating: 'calculating',
+ current_balance: 'Your current balance',
+ error_no_route: 'We were unable to find a route to your destination. Please try again later.',
+ error_not_enough_funds: 'You do not have enough funds available to make this payment.',
+ request_label_combined: 'Payment Request or Address',
+ request_label_offchain: 'Payment Request',
+ request_label_onchain: 'Address',
+ searching_routes: 'searching for routes',
+ next_block_confirmation: 'next block confirmation',
+ next: 'Next',
+ back: 'Back',
+ send: 'Send',
+ fee: 'Fee',
+ fee_range: 'between {minFee} and {maxFee} msat',
+ unknown: 'unknown',
+ amount: 'Amount',
+ per_byte: 'per byte',
+ upto: 'up to',
+ total: 'Total',
+ memo: 'Memo',
+ description:
+ 'You can send Bitcoin (BTC) through the Lightning Network or make a On-Chain Transaction. Just paste your Lightning Payment Request or the Bitcoin Address in the field below. Zap will guide you to the process.'
+})
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/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 (
{value &&
!fieldState.error && (
- Valid {isLn(value, chain, network) ? 'lightning' : chain} address{' '}
- {network !== 'mainnet' && `(${network})`}
+
)}
)
})
-export default LightningInvoiceInput
+export default injectIntl(LightningInvoiceInput)
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 75fd429c..8b435b8f 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'
@@ -27,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/app/components/UI/messages.js b/app/components/UI/messages.js
new file mode 100644
index 00000000..9357a041
--- /dev/null
+++ b/app/components/UI/messages.js
@@ -0,0 +1,9 @@
+import { defineMessages } from 'react-intl'
+
+/* eslint-disable max-len */
+export default defineMessages({
+ required_field: 'This is a required field',
+ invalid_request: 'Not a valid {chain} request.',
+ valid_request: 'Valid {chain} request',
+ payreq_placeholder: 'Paste a Lightning Payment Request or Bitcoin Address here'
+})
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)
+})
diff --git a/package.json b/package.json
index 89581c65..a5d1260b 100644
--- a/package.json
+++ b/package.json
@@ -310,6 +310,7 @@
"@rebass/components": "^4.0.0-1",
"axios": "^0.18.0",
"bitcoinjs-lib": "^4.0.1",
+ "bolt11": "https://github.com/bitcoinjs/bolt11.git",
"connected-react-router": "^4.5.0",
"copy-to-clipboard": "^3.0.8",
"country-data-lookup": "^0.0.3",
diff --git a/stories/.eslintrc b/stories/.eslintrc
new file mode 100644
index 00000000..1de5b1a7
--- /dev/null
+++ b/stories/.eslintrc
@@ -0,0 +1,5 @@
+{
+ "rules": {
+ "react/display-name": 0
+ }
+}
diff --git a/stories/pages/pay.stories.js b/stories/pages/pay.stories.js
new file mode 100644
index 00000000..22c91c63
--- /dev/null
+++ b/stories/pages/pay.stories.js
@@ -0,0 +1,296 @@
+/* eslint-disable max-len */
+
+import React from 'react'
+import { storiesOf } from '@storybook/react'
+import { action } from '@storybook/addon-actions'
+import { number, select, text } from '@storybook/addon-knobs'
+import { State, Store } from '@sambego/storybook-state'
+import { Modal, Page } from 'components/UI'
+import { Pay, PayButtons, PayHeader, PaySummaryLightning, PaySummaryOnChain } from 'components/Pay'
+
+const delay = time => new Promise(resolve => setTimeout(() => resolve(), time))
+
+const store = new Store({
+ chain: 'bitcoin',
+ network: 'testnet',
+ cryptoName: 'Bitcoin',
+ walletBalance: 10000000,
+ channelBalance: 25000,
+ cryptoCurrency: 'btc',
+ cryptoCurrencyTicker: 'BTC',
+ cryptoCurrencies: [
+ {
+ key: 'btc',
+ name: 'BTC'
+ },
+ {
+ key: 'bits',
+ name: 'bits'
+ },
+ {
+ key: 'sats',
+ name: 'satoshis'
+ }
+ ],
+
+ fiatCurrency: 'USD',
+ fiatCurrencies: ['USD', 'EUR', 'GBP'],
+
+ nodes: [
+ {
+ pub_key: '03c856d2dbec7454c48f311031f06bb99e3ca1ab15a9b9b35de14e139aa663b463',
+ alias: 'htlc.me'
+ }
+ ],
+ routes: [],
+
+ currentTicker: {
+ USD: {
+ last: 6477.78
+ },
+ EUR: {
+ last: 5656.01
+ },
+ GBP: {
+ last: 5052.73
+ }
+ },
+
+ isProcessing: false
+})
+
+const mockPayInvoice = async () => {
+ store.set({ isProcessing: true })
+ await delay(2000)
+ store.set({ isProcessing: false })
+}
+
+const mockSendCoins = async () => {
+ store.set({ isProcessing: true })
+ await delay(2000)
+ store.set({ isProcessing: false })
+}
+
+const mockQueryFees = async () => {
+ store.set({ isQueryingFees: true })
+ await delay(2000)
+ store.set({
+ onchainFees: {
+ fastestFee: 8,
+ halfHourFee: 8,
+ hourFee: 4
+ }
+ })
+ store.set({ isQueryingFees: false })
+}
+
+const mockQueryRoutes = async pubKey => {
+ store.set({ isQueryingRoutes: true })
+ await delay(2000)
+ const nodes = store.get('nodes')
+ if (nodes.find(n => n.pub_key === pubKey)) {
+ store.set({
+ routes: [
+ {
+ total_time_lock: 547118,
+ total_fees: '0',
+ total_amt: '10000',
+ hops: [
+ {
+ chan_id: '565542601916153857',
+ chan_capacity: '15698',
+ amt_to_forward: '10000',
+ fee: '0',
+ expiry: 546974,
+ amt_to_forward_msat: '10000010',
+ fee_msat: '21'
+ }
+ ],
+ total_fees_msat: '21',
+ total_amt_msat: '10000021'
+ },
+ {
+ total_time_lock: 547118,
+ total_fees: '0',
+ total_amt: '10000',
+ hops: [
+ {
+ chan_id: '565542601916153857',
+ chan_capacity: '15698',
+ amt_to_forward: '10000',
+ fee: '0',
+ expiry: 546974,
+ amt_to_forward_msat: '10000010',
+ fee_msat: '3'
+ }
+ ],
+ total_fees_msat: '3',
+ total_amt_msat: '10000021'
+ }
+ ]
+ })
+ } else {
+ store.set({ routes: [] })
+ }
+ store.set({ isQueryingRoutes: false })
+}
+
+const setCryptoCurrency = key => {
+ const items = store.get('cryptoCurrencies')
+ const item = items.find(i => i.key === key)
+ store.set({ cryptoCurrency: item.key })
+ store.set({ cryptoCurrencyTicker: item.name })
+}
+
+const setFiatCurrency = key => {
+ store.set({ fiatCurrency: key })
+}
+
+storiesOf('Containers.Pay', module)
+ .add('Pay', () => {
+ const network = select(
+ 'Network',
+ {
+ Testnet: 'testnet',
+ Mainnet: 'mainnet'
+ },
+ 'testnet'
+ )
+
+ return (
+
+
+
+
+
+
+
+ )
+ })
+ .addWithChapters('PayHeader', {
+ chapters: [
+ {
+ sections: [
+ {
+ title: 'On-chain',
+ sectionFn: () =>
+ },
+ {
+ title: 'Off-chain',
+ sectionFn: () =>
+ },
+ {
+ title: 'Generic',
+ sectionFn: () =>
+ }
+ ]
+ }
+ ]
+ })
+ .addWithChapters('PaySummary', {
+ chapters: [
+ {
+ sections: [
+ {
+ title: 'PaySummaryLightning',
+ sectionFn: () => (
+
+ ),
+ options: {
+ decorator: story => {story()}
+ }
+ },
+ {
+ title: 'PaySummaryOnChain',
+ sectionFn: () => {
+ mockQueryFees()
+ return (
+
+ )
+ },
+ options: {
+ decorator: story => {story()}
+ }
+ }
+ ]
+ }
+ ]
+ })
+ .addWithChapters('PayButtons', {
+ chapters: [
+ {
+ sections: [
+ {
+ title: 'Default',
+ sectionFn: () =>
+ },
+ {
+ title: 'Disabled',
+ sectionFn: () =>
+ },
+ {
+ title: 'Processing',
+ sectionFn: () =>
+ }
+ ]
+ }
+ ]
+ })
diff --git a/test/unit/__helpers__/setup-tests.js b/test/unit/__helpers__/setup-tests.js
index 5200d0bc..e084d8e3 100644
--- a/test/unit/__helpers__/setup-tests.js
+++ b/test/unit/__helpers__/setup-tests.js
@@ -1 +1,6 @@
import 'jest-styled-components'
+
+import { configure } from 'enzyme'
+import Adapter from 'enzyme-adapter-react-16'
+
+configure({ adapter: new Adapter() })
diff --git a/test/unit/components/Pay/PayButtons.spec.js b/test/unit/components/Pay/PayButtons.spec.js
new file mode 100644
index 00000000..58f774cc
--- /dev/null
+++ b/test/unit/components/Pay/PayButtons.spec.js
@@ -0,0 +1,11 @@
+import React from 'react'
+import { shallow } from 'enzyme'
+import toJSON from 'enzyme-to-json'
+import { PayButtons } from 'components/Pay'
+
+describe('component.Form.PayButtons', () => {
+ it('should render correctly with default props', () => {
+ const wrapper = shallow()
+ expect(toJSON(wrapper)).toMatchSnapshot()
+ })
+})
diff --git a/test/unit/components/Pay/PayHeader.spec.js b/test/unit/components/Pay/PayHeader.spec.js
new file mode 100644
index 00000000..0b49d833
--- /dev/null
+++ b/test/unit/components/Pay/PayHeader.spec.js
@@ -0,0 +1,25 @@
+import React from 'react'
+import { shallow } from 'enzyme'
+import toJSON from 'enzyme-to-json'
+import { PayHeader } from 'components/Pay'
+
+describe('component.Pay.PayHeader', () => {
+ describe('onchain', () => {
+ it('should render correctly', () => {
+ const wrapper = shallow()
+ expect(toJSON(wrapper)).toMatchSnapshot()
+ })
+ })
+ describe('offchain', () => {
+ it('should render correctly', () => {
+ const wrapper = shallow()
+ expect(toJSON(wrapper)).toMatchSnapshot()
+ })
+ })
+ describe('generic', () => {
+ it('should render correctly', () => {
+ const wrapper = shallow()
+ expect(toJSON(wrapper)).toMatchSnapshot()
+ })
+ })
+})
diff --git a/test/unit/components/Pay/PaySummaryLightning.spec.js b/test/unit/components/Pay/PaySummaryLightning.spec.js
new file mode 100644
index 00000000..79e3c1b0
--- /dev/null
+++ b/test/unit/components/Pay/PaySummaryLightning.spec.js
@@ -0,0 +1,45 @@
+import React from 'react'
+import { shallow } from 'enzyme'
+import toJSON from 'enzyme-to-json'
+import { PaySummaryLightning } from 'components/Pay'
+
+const props = {
+ currentTicker: {
+ USD: {
+ last: 6477.78
+ },
+ EUR: {
+ last: 5656.01
+ },
+ GBP: {
+ last: 5052.73
+ }
+ },
+ cryptoCurrency: 'btc',
+ cryptoCurrencyTicker: 'BTC',
+ cryptoCurrencies: [
+ {
+ key: 'btc',
+ name: 'BTC'
+ },
+ {
+ key: 'bits',
+ name: 'bits'
+ },
+ {
+ key: 'sats',
+ name: 'satoshis'
+ }
+ ],
+ fiatCurrency: 'USD',
+ /* eslint-disable max-len */
+ payReq:
+ 'lntb100u1pdaxza7pp5x73t3j7xgvkzgcdvzgpdg74k4pn0uhwuxlxu9qssytjn77x7zs4qdqqcqzysxqyz5vqd20eaq5uferzgzwasu5te3pla7gv8tzk8gcdxlj7lpkygvfdwndhwtl3ezn9ltjejl3hsp36ps3z3e5pp4rzp2hgqjqql80ec3hyzucq4d9axl',
+ setCryptoCurrency: jest.fn()
+}
+describe('component.Form.PaySummaryLightning', () => {
+ it('should render correctly', () => {
+ const wrapper = shallow()
+ expect(toJSON(wrapper)).toMatchSnapshot()
+ })
+})
diff --git a/test/unit/components/Pay/PaySummaryOnchain.spec.js b/test/unit/components/Pay/PaySummaryOnchain.spec.js
new file mode 100644
index 00000000..29a40140
--- /dev/null
+++ b/test/unit/components/Pay/PaySummaryOnchain.spec.js
@@ -0,0 +1,45 @@
+import React from 'react'
+import { shallow } from 'enzyme'
+import toJSON from 'enzyme-to-json'
+import { PaySummaryOnChain } from 'components/Pay'
+
+const props = {
+ amount: 1000,
+ address: 'mmxyr3LNKbnbrf6jdGXZpCE4EDpMSZRf4c',
+ currentTicker: {
+ USD: {
+ last: 6477.78
+ },
+ EUR: {
+ last: 5656.01
+ },
+ GBP: {
+ last: 5052.73
+ }
+ },
+ cryptoCurrency: 'btc',
+ cryptoCurrencyTicker: 'BTC',
+ cryptoCurrencies: [
+ {
+ key: 'btc',
+ name: 'BTC'
+ },
+ {
+ key: 'bits',
+ name: 'bits'
+ },
+ {
+ key: 'sats',
+ name: 'satoshis'
+ }
+ ],
+ fiatCurrency: 'USD',
+ setCryptoCurrency: jest.fn()
+}
+
+describe('component.Form.PaySummaryOnchain', () => {
+ it('should render correctly', () => {
+ const wrapper = shallow()
+ expect(toJSON(wrapper)).toMatchSnapshot()
+ })
+})
diff --git a/test/unit/components/Pay/PaySummaryRow.spec.js b/test/unit/components/Pay/PaySummaryRow.spec.js
new file mode 100644
index 00000000..fff9903c
--- /dev/null
+++ b/test/unit/components/Pay/PaySummaryRow.spec.js
@@ -0,0 +1,11 @@
+import React from 'react'
+import { shallow } from 'enzyme'
+import toJSON from 'enzyme-to-json'
+import { PaySummaryRow } from 'components/Pay'
+
+describe('component.Form.PaySummaryRow', () => {
+ it('should render correctly', () => {
+ const wrapper = shallow()
+ expect(toJSON(wrapper)).toMatchSnapshot()
+ })
+})
diff --git a/test/unit/components/Pay/__snapshots__/PayButtons.spec.js.snap b/test/unit/components/Pay/__snapshots__/PayButtons.spec.js.snap
new file mode 100644
index 00000000..c61005e3
--- /dev/null
+++ b/test/unit/components/Pay/__snapshots__/PayButtons.spec.js.snap
@@ -0,0 +1,54 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`component.Form.PayButtons should render correctly with default props 1`] = `
+
+
+
+
+
+
+
+`;
diff --git a/test/unit/components/Pay/__snapshots__/PayHeader.spec.js.snap b/test/unit/components/Pay/__snapshots__/PayHeader.spec.js.snap
new file mode 100644
index 00000000..677dcebc
--- /dev/null
+++ b/test/unit/components/Pay/__snapshots__/PayHeader.spec.js.snap
@@ -0,0 +1,96 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`component.Pay.PayHeader generic should render correctly 1`] = `
+
+
+
+
+
+ Send Bitcoin
+
+
+
+
+
+
+`;
+
+exports[`component.Pay.PayHeader offchain should render correctly 1`] = `
+
+
+
+
+
+ Send Bitcoin
+
+
+
+
+ Lightning Payment
+
+
+`;
+
+exports[`component.Pay.PayHeader onchain should render correctly 1`] = `
+
+
+
+
+
+ Send Bitcoin
+
+
+
+ On-Chain Payment
+
+
+
+`;
diff --git a/test/unit/components/Pay/__snapshots__/PaySummaryLightning.spec.js.snap b/test/unit/components/Pay/__snapshots__/PaySummaryLightning.spec.js.snap
new file mode 100644
index 00000000..2b7e6a7c
--- /dev/null
+++ b/test/unit/components/Pay/__snapshots__/PaySummaryLightning.spec.js.snap
@@ -0,0 +1,129 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`component.Form.PaySummaryLightning should render correctly 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ≈
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ right={
+
+ }
+ />
+
+
+ }
+ right={
+
+
+
+ BTC
+
+ }
+ />
+
+
+`;
diff --git a/test/unit/components/Pay/__snapshots__/PaySummaryOnchain.spec.js.snap b/test/unit/components/Pay/__snapshots__/PaySummaryOnchain.spec.js.snap
new file mode 100644
index 00000000..0849acec
--- /dev/null
+++ b/test/unit/components/Pay/__snapshots__/PaySummaryOnchain.spec.js.snap
@@ -0,0 +1,128 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`component.Form.PaySummaryOnchain should render correctly 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ≈
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ right={
+
+ }
+ />
+
+
+ }
+ right={
+
+
+
+ BTC
+
+ }
+ />
+
+`;
diff --git a/test/unit/components/Pay/__snapshots__/PaySummaryRow.spec.js.snap b/test/unit/components/Pay/__snapshots__/PaySummaryRow.spec.js.snap
new file mode 100644
index 00000000..f1f0f799
--- /dev/null
+++ b/test/unit/components/Pay/__snapshots__/PaySummaryRow.spec.js.snap
@@ -0,0 +1,30 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`component.Form.PaySummaryRow should render correctly 1`] = `
+
+
+
+
+ left contnet
+
+
+
+
+ right content
+
+
+
+
+`;
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/LightningInvoiceInput.spec.js b/test/unit/components/UI/LightningInvoiceInput.spec.js
index 17d8b59b..6361661b 100644
--- a/test/unit/components/UI/LightningInvoiceInput.spec.js
+++ b/test/unit/components/UI/LightningInvoiceInput.spec.js
@@ -4,16 +4,19 @@ import renderer from 'react-test-renderer'
import { dark } from 'themes'
import { ThemeProvider } from 'styled-components'
import { LightningInvoiceInput } from 'components/UI'
+import { IntlProvider } from 'react-intl'
describe('component.UI.LightningInvoiceInput', () => {
it('should render correctly', () => {
const tree = renderer
.create(
-
-
-
+
+
+
+
+
)
.toJSON()
expect(tree).toMatchSnapshot()
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__/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`] = `
+
+`;
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"`;
diff --git a/yarn.lock b/yarn.lock
index 7a0404d5..302ac2e0 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3849,6 +3849,11 @@ big.js@^3.1.3:
resolved "https://registry.yarnpkg.com/big.js/-/big.js-3.2.0.tgz#a5fc298b81b9e0dca2e458824784b65c52ba588e"
integrity sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==
+bigi@^1.1.0, bigi@^1.4.0:
+ version "1.4.2"
+ resolved "https://registry.yarnpkg.com/bigi/-/bigi-1.4.2.tgz#9c665a95f88b8b08fc05cfd731f561859d725825"
+ integrity sha1-nGZalfiLiwj8Bc/XMfVhhZ1yWCU=
+
binary-extensions@^1.0.0:
version "1.12.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.12.0.tgz#c2d780f53d45bba8317a8902d4ceeaf3a6385b14"
@@ -3862,7 +3867,7 @@ binary@^0.3.0, binary@~0.3.0:
buffers "~0.1.1"
chainsaw "~0.1.0"
-bindings@^1.3.0:
+bindings@^1.2.1, bindings@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.3.0.tgz#b346f6ecf6a95f5a815c5839fc7cdb22502f1ed7"
integrity sha512-DpLh5EzMR2kzvX1KIlVC0VkC3iZtHKTgdtZ0a3pglBZdaQFjt5S9g9xd1lE+YvXyfd6mtCeRnrUfOLYiTMlNSw==
@@ -3879,7 +3884,7 @@ bip32@^1.0.0:
typeforce "^1.11.5"
wif "^2.0.6"
-bip66@^1.1.0:
+bip66@^1.1.0, bip66@^1.1.3:
version "1.1.5"
resolved "https://registry.yarnpkg.com/bip66/-/bip66-1.1.5.tgz#01fa8748785ca70955d5011217d1b3139969ca22"
integrity sha1-AfqHSHhcpwlV1QESF9GzE5lpyiI=
@@ -3891,6 +3896,27 @@ bitcoin-ops@^1.3.0, bitcoin-ops@^1.4.0:
resolved "https://registry.yarnpkg.com/bitcoin-ops/-/bitcoin-ops-1.4.1.tgz#e45de620398e22fd4ca6023de43974ff42240278"
integrity sha512-pef6gxZFztEhaE9RY9HmWVmiIHqCb2OyS4HPKkpc6CIiiOa3Qmuoylxc5P2EkU3w+5eTSifI9SEZC88idAIGow==
+bitcoinjs-lib@^3.3.1:
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/bitcoinjs-lib/-/bitcoinjs-lib-3.3.2.tgz#780c9c53ecb1222adb463b58bef26386067b609a"
+ integrity sha512-l5qqvbaK8wwtANPf6oEffykycg4383XgEYdia1rI7/JpGf1jfRWlOUCvx5TiTZS7kyIvY4j/UhIQ2urLsvGkzw==
+ dependencies:
+ bech32 "^1.1.2"
+ bigi "^1.4.0"
+ bip66 "^1.1.0"
+ bitcoin-ops "^1.3.0"
+ bs58check "^2.0.0"
+ create-hash "^1.1.0"
+ create-hmac "^1.1.3"
+ ecurve "^1.0.0"
+ merkle-lib "^2.0.10"
+ pushdata-bitcoin "^1.0.1"
+ randombytes "^2.0.1"
+ safe-buffer "^5.0.1"
+ typeforce "^1.11.3"
+ varuint-bitcoin "^1.0.4"
+ wif "^2.0.1"
+
bitcoinjs-lib@^4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/bitcoinjs-lib/-/bitcoinjs-lib-4.0.2.tgz#84fc9d14774c07581ad1795f2444706fc3f7a666"
@@ -3944,7 +3970,7 @@ bluebird@~3.4.1:
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.4.7.tgz#f72d760be09b7f76d08ed8fae98b289a8d05fab3"
integrity sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=
-bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.11.8, bn.js@^4.4.0:
+bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.11.3, bn.js@^4.11.8, bn.js@^4.4.0:
version "4.11.8"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==
@@ -3965,6 +3991,17 @@ body-parser@1.18.3:
raw-body "2.3.3"
type-is "~1.6.16"
+"bolt11@https://github.com/bitcoinjs/bolt11.git":
+ version "1.0.0"
+ resolved "https://github.com/bitcoinjs/bolt11.git#d543a7aba5c3dd747627f523187abaf85976f19c"
+ dependencies:
+ bech32 "^1.1.2"
+ bitcoinjs-lib "^3.3.1"
+ bn.js "^4.11.8"
+ lodash "^4.17.4"
+ safe-buffer "^5.1.1"
+ secp256k1 "^3.4.0"
+
bonjour@^3.5.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5"
@@ -4068,7 +4105,7 @@ browser-resolve@^1.11.3:
dependencies:
resolve "1.1.7"
-browserify-aes@^1.0.0, browserify-aes@^1.0.4:
+browserify-aes@^1.0.0, browserify-aes@^1.0.4, browserify-aes@^1.0.6:
version "1.2.0"
resolved "https://registry.yarnpkg.com/browserify-aes/-/browserify-aes-1.2.0.tgz#326734642f403dabc3003209853bb70ad428ef48"
integrity sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==
@@ -6116,6 +6153,15 @@ downshift@^3.1.4:
prop-types "^15.6.0"
react-is "^16.5.2"
+drbg.js@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/drbg.js/-/drbg.js-1.0.1.tgz#3e36b6c42b37043823cdbc332d58f31e2445480b"
+ integrity sha1-Pja2xCs3BDgjzbwzLVjzHiRFSAs=
+ dependencies:
+ browserify-aes "^1.0.6"
+ create-hash "^1.1.2"
+ create-hmac "^1.1.4"
+
duplexer2@~0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/duplexer2/-/duplexer2-0.1.4.tgz#8b12dab878c0d69e3e7891051662a32fc6bddcc1"
@@ -6151,6 +6197,14 @@ ecc-jsbn@~0.1.1:
jsbn "~0.1.0"
safer-buffer "^2.1.0"
+ecurve@^1.0.0:
+ version "1.0.6"
+ resolved "https://registry.yarnpkg.com/ecurve/-/ecurve-1.0.6.tgz#dfdabbb7149f8d8b78816be5a7d5b83fcf6de797"
+ integrity sha512-/BzEjNfiSuB7jIWKcS/z8FK9jNjmEWvUV2YZ4RLSmcDtP7Lq0m6FvDuSnJpBlDpGRpfRQeTLGLBI8H+kEv0r+w==
+ dependencies:
+ bigi "^1.1.0"
+ safe-buffer "^5.0.1"
+
ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
@@ -6292,7 +6346,7 @@ elegant-spinner@^1.0.1:
resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e"
integrity sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4=
-elliptic@^6.0.0, elliptic@^6.4.0:
+elliptic@^6.0.0, elliptic@^6.2.3, elliptic@^6.4.0:
version "6.4.1"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.1.tgz#c2d0b7776911b86722c632c3c06c60f2f819939a"
integrity sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==
@@ -11219,7 +11273,7 @@ mute-stream@0.0.7:
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=
-nan@^2.10.0, nan@^2.9.2:
+nan@^2.10.0, nan@^2.2.1, nan@^2.9.2:
version "2.11.1"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.11.1.tgz#90e22bccb8ca57ea4cd37cc83d3819b52eea6766"
integrity sha512-iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA==
@@ -14496,6 +14550,20 @@ seamless-immutable@^7.1.2, seamless-immutable@^7.1.3:
resolved "https://registry.yarnpkg.com/seamless-immutable/-/seamless-immutable-7.1.4.tgz#6e9536def083ddc4dea0207d722e0e80d0f372f8"
integrity sha512-XiUO1QP4ki4E2PHegiGAlu6r82o5A+6tRh7IkGGTVg/h+UoeX4nFBeCGPOhb4CYjvkqsfm/TUtvOMYC1xmV30A==
+secp256k1@^3.4.0:
+ version "3.5.2"
+ resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-3.5.2.tgz#f95f952057310722184fe9c914e6b71281f2f2ae"
+ integrity sha512-iin3kojdybY6NArd+UFsoTuapOF7bnJNf2UbcWXaY3z+E1sJDipl60vtzB5hbO/uquBu7z0fd4VC4Irp+xoFVQ==
+ dependencies:
+ bindings "^1.2.1"
+ bip66 "^1.1.3"
+ bn.js "^4.11.3"
+ create-hash "^1.1.2"
+ drbg.js "^1.0.1"
+ elliptic "^6.2.3"
+ nan "^2.2.1"
+ safe-buffer "^5.1.0"
+
select-hose@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"