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 ( +
+ {({ formState }) => { + // Deterine which buttons should be visible. + const showBack = currentStep !== 'address' + const showSubmit = currentStep !== 'address' || (isOnchain || isLn) + + // Determine wether we have a route to the sender. + let hasRoute = true + if (isLn && currentStep === 'summary') { + const { min, max } = getFeeRange(routes || []) + if (min === null || max === null) { + hasRoute = false + } + } + + // Determine wether we have enough funds available. + let hasEnoughFunds = true + if (isLn && invoice) { + hasEnoughFunds = invoice.satoshis <= channelBalance + } else if (isOnchain) { + const valueInSats = convert(cryptoCurrency, 'sats', formState.values.amountCrypto) + hasEnoughFunds = valueInSats <= walletBalance + } + + // Determine what the text should be for the next button. + let nextButtonText = intl.formatMessage({ ...messages.next }) + if (currentStep === 'summary') { + const value = + isLn && invoice + ? convert('sats', cryptoCurrency, invoice.satoshis) + : formState.values.amountCrypto + nextButtonText = `${intl.formatMessage({ + ...messages.send + })} ${value} ${cryptoCurrencyTicker}` + } + + return ( + + + + + + + + {this.renderHelpText()} + {this.renderAddressField()} + {isOnchain && this.renderAmountFields()} + {currentStep === 'summary' && this.renderSummary()} + + + + + {styles => ( + + {currentStep === 'summary' && + !isQueryingRoutes && + !hasRoute && ( + + + + )} + + {currentStep === 'summary' && + !hasEnoughFunds && ( + + + + )} + + + + {walletBalance !== null && ( + + + : + + + {convert('sats', cryptoCurrency, walletBalance)} + {` `} + {cryptoCurrencyTicker} (onchain), + + + {convert('sats', cryptoCurrency, channelBalance)} + {` `} + {cryptoCurrencyTicker} (in channels) + + + )} + + )} + + + + ) + }} +
+ ) + } +} + +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 (