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 (