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 { decodePayReq, getMinFee, getMaxFee, getFeeRange, isOnchain, isLn } from 'lib/utils/crypto'
import { convert } from 'lib/utils/btc'
import {
Bar,
CryptoAmountInput,
Dropdown,
FiatAmountInput,
Form,
Message,
Label,
LightningInvoiceInput,
Panel,
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, immediate: true })
}
})
/**
* 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 = {
initialPayReq: null,
initialAmountCrypto: null,
initialAmountFiat: null,
isProcessing: false,
isQueryingFees: false,
isQueryingRoutes: false,
nodes: [],
onchainFees: {},
routes: []
}
state = {
currentStep: 'address',
previousStep: null,
isLn: null,
isOnchain: null
}
amountInput = React.createRef()
payReqInput = React.createRef()
componentDidUpdate(prevProps, prevState) {
const { initialPayReq, queryRoutes } = this.props
const { currentStep, invoice, isLn, isOnchain } = this.state
// If initialPayReq has been set, reset the form and submit as new
if (initialPayReq && initialPayReq !== prevProps.initialPayReq) {
this.formApi.reset()
this.formApi.setValue('payReq', initialPayReq)
this.handlePayReqChange()
}
// If we have gone back to the address step, unmark all fields from being touched.
if (currentStep !== prevState.currentStep) {
if (currentStep === 'address') {
Object.keys(this.formApi.getState().touched).forEach(field => {
this.formApi.setTouched(field, false)
})
}
}
// If we now have a valid onchain address, trigger the form submit.
if (isOnchain && isOnchain !== prevState.isOnchain) {
this.formApi.submitForm()
}
// If we now have a valid offchain address, trigger the form submit.
if (isLn && isLn !== prevState.isLn) {
this.formApi.submitForm()
// And if now have a valid lightning invoice, call queryRoutes.
if (invoice) {
const { satoshis, payeeNodeKey } = invoice
queryRoutes(payeeNodeKey, satoshis)
}
}
}
/**
* Form submit handler.
* @param {Object} values submitted form values.
*/
onSubmit = values => {
const { currentStep, isOnchain } = this.state
const { cryptoCurrency, onchainFees, payInvoice, routes, sendCoins } = this.props
const feeLimit = getMaxFee(routes)
if (currentStep === 'summary') {
return isOnchain
? sendCoins({
value: values.amountCrypto,
addr: values.payReq,
currency: cryptoCurrency,
satPerByte: onchainFees.fastestFee
})
: payInvoice(values.payReq, feeLimit)
} else {
this.nextStep()
}
}
/**
* Store the formApi on the component context to make it available at this.formApi.
*/
setFormApi = formApi => {
this.formApi = formApi
}
/**
* 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], previousStep: currentStep })
}
}
/**
* 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], previousStep: currentStep })
}
}
/**
* Set isLn/isOnchain state based on payReq value.
*/
handlePayReqChange = () => {
const { chain, network } = 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 = decodePayReq(payReq)
state.invoice = invoice
} catch (e) {
return
}
state.isLn = true
}
// 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)
}
/**
* Handle the case when the form is mountedwith an initialPayReq.
* This is the earliest possibleplace we can do this because the form is not initialised in ComponentDidMount.
*/
handleChange = formState => {
const { initialPayReq } = this.props
const { currentStep, previousStep } = this.state
// If this is the first time the address page is showing and we have an initialPayReq, process the request
// as if the user had entered it themselves.
if (currentStep === 'address' && !previousStep && initialPayReq && formState.values.payReq) {
this.handlePayReqChange()
}
}
/**
* set the amountFiat field whenever the crypto amount changes.
*/
handleAmountCryptoChange = e => {
const { cryptoCurrency, currentTicker, fiatCurrency } = this.props
const lastPrice = currentTicker[fiatCurrency]
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]
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 { initialPayReq } = this.props
const { currentStep, previousStep } = this.state
// Do not render the help text if the form has just loadad with an initial payment request.
if (initialPayReq && !previousStep) {
return null
}
return (