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