JimmyMow
6 years ago
committed by
GitHub
14 changed files with 15 additions and 786 deletions
@ -1,217 +0,0 @@ |
|||||
import React, { Component } from 'react' |
|
||||
import PropTypes from 'prop-types' |
|
||||
import { btc } from 'lib/utils' |
|
||||
import PaperPlane from 'components/Icon/PaperPlane' |
|
||||
import ChainLink from 'components/Icon/ChainLink' |
|
||||
import AmountInput from 'components/AmountInput' |
|
||||
import { Button, Dropdown } from 'components/UI' |
|
||||
import { FormattedNumber, FormattedMessage, injectIntl } from 'react-intl' |
|
||||
import messages from './messages' |
|
||||
import styles from './Pay.scss' |
|
||||
|
|
||||
class Pay extends Component { |
|
||||
constructor(props) { |
|
||||
super(props) |
|
||||
this.paymentRequestInput = React.createRef() |
|
||||
} |
|
||||
|
|
||||
componentDidMount() { |
|
||||
const { setPayInput, setPayAmount } = this.props |
|
||||
|
|
||||
// Clear the form of any previous data.
|
|
||||
setPayInput('') |
|
||||
setPayAmount('') |
|
||||
|
|
||||
// Focus the payment request input field.
|
|
||||
this.paymentRequestInput.current.focus() |
|
||||
} |
|
||||
|
|
||||
componentDidUpdate(prevProps) { |
|
||||
const { |
|
||||
isLn, |
|
||||
payform: { payInput }, |
|
||||
fetchInvoice |
|
||||
} = this.props |
|
||||
|
|
||||
// If LN go retrieve invoice details
|
|
||||
if (prevProps.payform.payInput !== payInput && isLn) { |
|
||||
fetchInvoice(payInput) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
render() { |
|
||||
const { |
|
||||
payform: { payInput, showErrors, invoice }, |
|
||||
nodes, |
|
||||
ticker, |
|
||||
isOnchain, |
|
||||
isLn, |
|
||||
currentAmount, |
|
||||
fiatAmount, |
|
||||
payFormIsValid: { errors, isValid }, |
|
||||
currencyFilters, |
|
||||
setPayAmount, |
|
||||
onPayAmountBlur, |
|
||||
setPayInput, |
|
||||
onPayInputBlur, |
|
||||
onPaySubmit, |
|
||||
setCurrency, |
|
||||
intl |
|
||||
} = this.props |
|
||||
|
|
||||
const displayNodeName = pubkey => { |
|
||||
const node = nodes.find(n => n.pub_key === pubkey) |
|
||||
|
|
||||
if (node && node.alias.length) { |
|
||||
return node.alias |
|
||||
} |
|
||||
|
|
||||
return pubkey ? pubkey.substring(0, 10) : '' |
|
||||
} |
|
||||
|
|
||||
const onCurrencyFilterClick = currency => { |
|
||||
if (!isLn) { |
|
||||
// change the input amount
|
|
||||
setPayAmount(btc.convert(ticker.currency, currency, currentAmount)) |
|
||||
} |
|
||||
setCurrency(currency) |
|
||||
} |
|
||||
|
|
||||
return ( |
|
||||
<div className={styles.container}> |
|
||||
<header className={styles.header}> |
|
||||
<PaperPlane width="2em" height="2em" /> |
|
||||
<h1> |
|
||||
<FormattedMessage {...messages.title} /> |
|
||||
</h1> |
|
||||
</header> |
|
||||
|
|
||||
<div className={styles.content}> |
|
||||
<section className={styles.destination}> |
|
||||
<div className={styles.top}> |
|
||||
<label htmlFor="paymentRequest"> |
|
||||
<FormattedMessage {...messages.destination} /> |
|
||||
</label> |
|
||||
<span |
|
||||
className={`${styles.description} ${isOnchain || isLn ? styles.active : undefined}`} |
|
||||
> |
|
||||
{isOnchain && ( |
|
||||
<i> |
|
||||
<ChainLink /> |
|
||||
<span> |
|
||||
<FormattedMessage {...messages.onchain_description} /> |
|
||||
</span> |
|
||||
</i> |
|
||||
)} |
|
||||
{isLn && ( |
|
||||
<i> |
|
||||
<span> |
|
||||
{displayNodeName(invoice.destination)} ({invoice.description}) |
|
||||
</span> |
|
||||
</i> |
|
||||
)} |
|
||||
</span> |
|
||||
</div> |
|
||||
<div className={styles.bottom}> |
|
||||
<textarea |
|
||||
type="text" |
|
||||
placeholder={intl.formatMessage({ ...messages.request_placeholder })} |
|
||||
value={payInput} |
|
||||
onChange={event => setPayInput(event.target.value)} |
|
||||
onBlur={onPayInputBlur} |
|
||||
id="paymentRequest" |
|
||||
rows="4" |
|
||||
ref={this.paymentRequestInput} |
|
||||
/> |
|
||||
<section |
|
||||
className={`${styles.errorMessage} ${ |
|
||||
showErrors.payInput ? styles.active : undefined |
|
||||
}`}
|
|
||||
> |
|
||||
{showErrors.payInput && <span>{errors.payInput}</span>} |
|
||||
</section> |
|
||||
</div> |
|
||||
</section> |
|
||||
|
|
||||
<section className={styles.amount}> |
|
||||
<div className={styles.top}> |
|
||||
<label htmlFor="amount"> |
|
||||
<FormattedMessage {...messages.amount} /> |
|
||||
</label> |
|
||||
<span /> |
|
||||
</div> |
|
||||
<div className={styles.bottom}> |
|
||||
<AmountInput |
|
||||
id="amount" |
|
||||
amount={currentAmount} |
|
||||
currency={ticker.currency} |
|
||||
onChangeEvent={setPayAmount} |
|
||||
onBlurEvent={onPayAmountBlur} |
|
||||
readOnly={isLn} |
|
||||
/> |
|
||||
<Dropdown |
|
||||
activeKey={ticker.currency} |
|
||||
items={currencyFilters} |
|
||||
onChange={onCurrencyFilterClick} |
|
||||
ml={2} |
|
||||
/> |
|
||||
</div> |
|
||||
|
|
||||
<div className={styles.fiatAmount}> |
|
||||
{'≈ '} |
|
||||
<FormattedNumber |
|
||||
currency={ticker.fiatTicker} |
|
||||
style="currency" |
|
||||
value={fiatAmount || 0} |
|
||||
/> |
|
||||
</div> |
|
||||
|
|
||||
<section |
|
||||
className={`${styles.errorMessage} ${styles.amount} ${ |
|
||||
showErrors.amount ? styles.active : undefined |
|
||||
}`}
|
|
||||
> |
|
||||
{showErrors.amount && <span>{errors.amount}</span>} |
|
||||
</section> |
|
||||
</section> |
|
||||
|
|
||||
<section className={styles.submit}> |
|
||||
<Button disabled={!isValid} onClick={onPaySubmit} width={200}> |
|
||||
<FormattedMessage {...messages.pay} /> |
|
||||
</Button> |
|
||||
</section> |
|
||||
</div> |
|
||||
</div> |
|
||||
) |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
Pay.propTypes = { |
|
||||
payform: PropTypes.shape({ |
|
||||
amount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), |
|
||||
payInput: PropTypes.string.isRequired, |
|
||||
invoice: PropTypes.object.isRequired, |
|
||||
showErrors: PropTypes.object.isRequired |
|
||||
}).isRequired, |
|
||||
currencyName: PropTypes.string.isRequired, |
|
||||
isOnchain: PropTypes.bool.isRequired, |
|
||||
isLn: PropTypes.bool.isRequired, |
|
||||
currentAmount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), |
|
||||
fiatAmount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), |
|
||||
payFormIsValid: PropTypes.shape({ |
|
||||
errors: PropTypes.object, |
|
||||
isValid: PropTypes.bool |
|
||||
}).isRequired, |
|
||||
setPayAmount: PropTypes.func.isRequired, |
|
||||
onPayAmountBlur: PropTypes.func.isRequired, |
|
||||
setPayInput: PropTypes.func.isRequired, |
|
||||
onPayInputBlur: PropTypes.func.isRequired, |
|
||||
fetchInvoice: PropTypes.func.isRequired, |
|
||||
onPaySubmit: PropTypes.func.isRequired, |
|
||||
setCurrency: PropTypes.func.isRequired, |
|
||||
ticker: PropTypes.object.isRequired, |
|
||||
nodes: PropTypes.array.isRequired, |
|
||||
currencyFilters: PropTypes.array.isRequired |
|
||||
} |
|
||||
|
|
||||
export default injectIntl(Pay) |
|
@ -1,122 +0,0 @@ |
|||||
@import 'styles/variables.scss'; |
|
||||
|
|
||||
.container { |
|
||||
margin: 0 auto; |
|
||||
width: 500px; |
|
||||
} |
|
||||
|
|
||||
.header { |
|
||||
text-align: center; |
|
||||
padding-bottom: 20px; |
|
||||
color: var(--primaryText); |
|
||||
border-bottom: 1px solid $spaceborder; |
|
||||
|
|
||||
h1 { |
|
||||
font-size: 22px; |
|
||||
font-weight: 100; |
|
||||
margin-top: 10px; |
|
||||
letter-spacing: 1.5px; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.content { |
|
||||
margin-top: 40px; |
|
||||
color: var(--primaryText); |
|
||||
|
|
||||
.destination { |
|
||||
margin-bottom: 10px; |
|
||||
|
|
||||
.description { |
|
||||
font-size: 12px; |
|
||||
line-height: 14px; |
|
||||
padding: 0 15px; |
|
||||
min-height: 14px; |
|
||||
|
|
||||
&.active { |
|
||||
background: var(--lightBackground); |
|
||||
border-radius: 10px; |
|
||||
min-height: 0; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
svg { |
|
||||
width: 10px; |
|
||||
height: 10px; |
|
||||
line-height: 14px; |
|
||||
margin-right: 5px; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.amount .bottom { |
|
||||
display: flex; |
|
||||
align-items: center; |
|
||||
|
|
||||
input { |
|
||||
font-size: 20px; |
|
||||
max-width: 150px; |
|
||||
border: 1px solid #404040; |
|
||||
border-radius: 4px; |
|
||||
padding: 15px; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.top { |
|
||||
margin-bottom: 10px; |
|
||||
display: flex; |
|
||||
flex-direction: row; |
|
||||
justify-content: space-between; |
|
||||
align-items: center; |
|
||||
|
|
||||
label { |
|
||||
font-size: 14px; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.bottom { |
|
||||
input, |
|
||||
textarea { |
|
||||
background: transparent; |
|
||||
outline: none; |
|
||||
border: 1px solid #404040; |
|
||||
color: var(--lightningOrange); |
|
||||
-webkit-text-fill-color: var(--primaryText); |
|
||||
font-size: 12px; |
|
||||
width: 100%; |
|
||||
font-weight: 200; |
|
||||
padding: 10px; |
|
||||
} |
|
||||
|
|
||||
input::-webkit-input-placeholder, |
|
||||
textarea::-webkit-input-placeholder { |
|
||||
text-shadow: none; |
|
||||
-webkit-text-fill-color: initial; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.fiatAmount { |
|
||||
margin-top: 10px; |
|
||||
opacity: 0.5; |
|
||||
font-size: 14px; |
|
||||
} |
|
||||
|
|
||||
.submit { |
|
||||
margin-top: 20px; |
|
||||
text-align: center; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
.errorMessage { |
|
||||
color: var(--superRed); |
|
||||
font-size: 12px; |
|
||||
min-height: 20px; |
|
||||
opacity: 0; |
|
||||
transition: all 0.25s ease; |
|
||||
|
|
||||
&.amount { |
|
||||
margin-top: 10px; |
|
||||
} |
|
||||
|
|
||||
&.active { |
|
||||
opacity: 1; |
|
||||
} |
|
||||
} |
|
@ -1,3 +0,0 @@ |
|||||
import Pay from './Pay' |
|
||||
|
|
||||
export default Pay |
|
@ -1,11 +0,0 @@ |
|||||
import { defineMessages } from 'react-intl' |
|
||||
|
|
||||
/* eslint-disable max-len */ |
|
||||
export default defineMessages({ |
|
||||
title: 'Make Payment', |
|
||||
destination: 'Destination', |
|
||||
amount: 'Amount', |
|
||||
request_placeholder: 'Paste payment request or bitcoin address here', |
|
||||
pay: 'Pay', |
|
||||
onchain_description: 'On-Chain (~10 minutes)' |
|
||||
}) |
|
@ -1,268 +0,0 @@ |
|||||
import { createSelector } from 'reselect' |
|
||||
import bitcoin from 'bitcoinjs-lib' |
|
||||
import { btc, bech32 } from 'lib/utils' |
|
||||
import { setFormType } from './form' |
|
||||
import { tickerSelectors } from './ticker' |
|
||||
import { infoSelectors } from './info' |
|
||||
|
|
||||
// Initial State
|
|
||||
const initialState = { |
|
||||
amount: '', |
|
||||
payInput: '', |
|
||||
|
|
||||
invoice: { |
|
||||
payreq: '', |
|
||||
r_hash: '', |
|
||||
amount: '0', |
|
||||
description: '', |
|
||||
destination: '' |
|
||||
}, |
|
||||
|
|
||||
showErrors: { |
|
||||
amount: false, |
|
||||
payInput: false |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// Constants
|
|
||||
// ------------------------------------
|
|
||||
export const SET_PAY_AMOUNT = 'SET_PAY_AMOUNT' |
|
||||
export const SET_PAY_INPUT = 'SET_PAY_INPUT' |
|
||||
export const SET_PAY_INVOICE = 'SET_PAY_INVOICE' |
|
||||
export const UPDATE_PAY_ERRORS = 'UPDATE_PAY_ERRORS' |
|
||||
export const RESET_FORM = 'RESET_FORM' |
|
||||
|
|
||||
// ------------------------------------
|
|
||||
// Actions
|
|
||||
// ------------------------------------
|
|
||||
export function setPayAmount(amount) { |
|
||||
return { |
|
||||
type: SET_PAY_AMOUNT, |
|
||||
amount |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export function setPayInput(payInput) { |
|
||||
return { |
|
||||
type: SET_PAY_INPUT, |
|
||||
payInput |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export function setPayInvoice(invoice) { |
|
||||
return { |
|
||||
type: SET_PAY_INVOICE, |
|
||||
invoice |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export function updatePayErrors(errorsObject) { |
|
||||
return { |
|
||||
type: UPDATE_PAY_ERRORS, |
|
||||
errorsObject |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
export const lightningPaymentUri = (event, { payreq }) => dispatch => { |
|
||||
// Open pay form
|
|
||||
dispatch(setFormType('PAY_FORM')) |
|
||||
// Set payreq
|
|
||||
dispatch(setPayInput(payreq)) |
|
||||
} |
|
||||
|
|
||||
export function resetPayForm() { |
|
||||
return { |
|
||||
type: RESET_FORM |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
// ------------------------------------
|
|
||||
// Action Handlers
|
|
||||
// ------------------------------------
|
|
||||
const ACTION_HANDLERS = { |
|
||||
[SET_PAY_AMOUNT]: (state, { amount }) => ({ |
|
||||
...state, |
|
||||
amount, |
|
||||
showErrors: Object.assign(state.showErrors, { amount: false }) |
|
||||
}), |
|
||||
[SET_PAY_INPUT]: (state, { payInput }) => ({ |
|
||||
...state, |
|
||||
payInput, |
|
||||
showErrors: Object.assign(state.showErrors, { payInput: false }) |
|
||||
}), |
|
||||
[SET_PAY_INVOICE]: (state, { invoice }) => ({ |
|
||||
...state, |
|
||||
invoice, |
|
||||
showErrors: Object.assign(state.showErrors, { amount: false }) |
|
||||
}), |
|
||||
|
|
||||
[UPDATE_PAY_ERRORS]: (state, { errorsObject }) => ({ |
|
||||
...state, |
|
||||
showErrors: Object.assign(state.showErrors, errorsObject) |
|
||||
}), |
|
||||
|
|
||||
[RESET_FORM]: () => initialState |
|
||||
} |
|
||||
|
|
||||
// ------------------------------------
|
|
||||
// Selector
|
|
||||
// ------------------------------------
|
|
||||
const payFormSelectors = {} |
|
||||
const payAmountSelector = state => state.payform.amount |
|
||||
const payInputSelector = state => state.payform.payInput |
|
||||
const payInvoiceSelector = state => state.payform.invoice |
|
||||
|
|
||||
// transaction
|
|
||||
const sendingTransactionSelector = state => state.transaction.sendingTransaction |
|
||||
|
|
||||
// payment
|
|
||||
const sendingPaymentSelector = state => state.payment.sendingPayment |
|
||||
|
|
||||
// ticker
|
|
||||
const currencySelector = state => state.ticker.currency |
|
||||
const fiatTickerSelector = state => state.ticker.fiatTicker |
|
||||
|
|
||||
payFormSelectors.isOnchain = createSelector( |
|
||||
payInputSelector, |
|
||||
infoSelectors.networkSelector, |
|
||||
(input, network) => { |
|
||||
try { |
|
||||
bitcoin.address.toOutputScript(input, network.bitcoinJsNetwork) |
|
||||
return true |
|
||||
} catch (e) { |
|
||||
return false |
|
||||
} |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
payFormSelectors.isLn = createSelector(payInputSelector, input => { |
|
||||
if (!input.startsWith('ln')) { |
|
||||
return false |
|
||||
} |
|
||||
|
|
||||
try { |
|
||||
bech32.decode(input) |
|
||||
return true |
|
||||
} catch (e) { |
|
||||
return false |
|
||||
} |
|
||||
}) |
|
||||
|
|
||||
payFormSelectors.currentAmount = createSelector( |
|
||||
payFormSelectors.isLn, |
|
||||
payAmountSelector, |
|
||||
payInvoiceSelector, |
|
||||
currencySelector, |
|
||||
(isLn, amount, invoice, currency) => { |
|
||||
if (isLn) { |
|
||||
switch (currency) { |
|
||||
case 'btc': |
|
||||
return btc.satoshisToBtc(invoice.num_satoshis || 0) |
|
||||
case 'bits': |
|
||||
return btc.satoshisToBits(invoice.num_satoshis || 0) |
|
||||
case 'sats': |
|
||||
return invoice.num_satoshis |
|
||||
default: |
|
||||
return invoice.num_satoshis |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
return amount |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
payFormSelectors.fiatAmount = createSelector( |
|
||||
payFormSelectors.isLn, |
|
||||
payAmountSelector, |
|
||||
payInvoiceSelector, |
|
||||
currencySelector, |
|
||||
tickerSelectors.currentTicker, |
|
||||
fiatTickerSelector, |
|
||||
(isLn, amount, invoice, currency, currentTicker, fiatTicker) => { |
|
||||
if (!currentTicker || !currentTicker[fiatTicker].last) { |
|
||||
return false |
|
||||
} |
|
||||
|
|
||||
if (isLn) { |
|
||||
return btc.satoshisToFiat(invoice.num_satoshis || 0, currentTicker[fiatTicker].last) |
|
||||
} |
|
||||
|
|
||||
return btc.convert(currency, 'fiat', amount, currentTicker[fiatTicker].last) |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
payFormSelectors.payInputMin = createSelector(currencySelector, currency => { |
|
||||
switch (currency) { |
|
||||
case 'btc': |
|
||||
return '0.00000001' |
|
||||
case 'bits': |
|
||||
return '0.01' |
|
||||
case 'sats': |
|
||||
return '1' |
|
||||
default: |
|
||||
return '0' |
|
||||
} |
|
||||
}) |
|
||||
|
|
||||
payFormSelectors.inputCaption = createSelector( |
|
||||
payFormSelectors.isOnchain, |
|
||||
payFormSelectors.isLn, |
|
||||
payFormSelectors.currentAmount, |
|
||||
currencySelector, |
|
||||
(isOnchain, isLn, amount, currency) => { |
|
||||
if (!isOnchain && !isLn) { |
|
||||
return '' |
|
||||
} |
|
||||
|
|
||||
if (isOnchain) { |
|
||||
return `You're about to send ${amount} ${currency.toUpperCase()} on-chain which should take around 10 minutes` |
|
||||
} |
|
||||
|
|
||||
if (isLn) { |
|
||||
return `You're about to send ${amount} ${currency.toUpperCase()} over the Lightning Network which will be instant` |
|
||||
} |
|
||||
|
|
||||
return '' |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
payFormSelectors.showPayLoadingScreen = createSelector( |
|
||||
sendingTransactionSelector, |
|
||||
sendingPaymentSelector, |
|
||||
(sendingTransaction, sendingPayment) => sendingTransaction || sendingPayment |
|
||||
) |
|
||||
|
|
||||
payFormSelectors.payFormIsValid = createSelector( |
|
||||
payFormSelectors.isOnchain, |
|
||||
payFormSelectors.isLn, |
|
||||
payAmountSelector, |
|
||||
(isOnchain, isLn, amount) => { |
|
||||
const errors = {} |
|
||||
|
|
||||
if (!isLn && amount <= 0) { |
|
||||
errors.amount = 'Amount must be more than 0' |
|
||||
} |
|
||||
|
|
||||
if (!isOnchain && !isLn) { |
|
||||
errors.payInput = 'Must be a valid BTC address or Lightning Network request' |
|
||||
} |
|
||||
|
|
||||
return { |
|
||||
errors, |
|
||||
amountIsValid: !errors.amount, |
|
||||
payInputIsValid: !errors.payInput, |
|
||||
isValid: Object.keys(errors).length === 0 |
|
||||
} |
|
||||
} |
|
||||
) |
|
||||
|
|
||||
export { payFormSelectors } |
|
||||
|
|
||||
// ------------------------------------
|
|
||||
// Reducer
|
|
||||
// ------------------------------------
|
|
||||
export default function payFormReducer(state = initialState, action) { |
|
||||
const handler = ACTION_HANDLERS[action.type] |
|
||||
|
|
||||
return handler ? handler(state, action) : state |
|
||||
} |
|
Loading…
Reference in new issue