Browse Source

feature(payform): add multi-currency support for payform

renovate/lint-staged-8.x
Jack Mallers 7 years ago
parent
commit
0883bbd486
  1. 48
      app/components/Form/Pay.js
  2. 18
      app/components/Form/Pay.scss
  3. 55
      app/reducers/payform.js
  4. 34
      app/reducers/ticker.js
  5. 12
      app/routes/app/containers/AppContainer.js
  6. 99
      app/utils/btc.js

48
app/components/Form/Pay.js

@ -5,8 +5,9 @@ import find from 'lodash/find'
import Isvg from 'react-inlinesvg' import Isvg from 'react-inlinesvg'
import paperPlane from 'icons/paper_plane.svg' import paperPlane from 'icons/paper_plane.svg'
import link from 'icons/link.svg' import link from 'icons/link.svg'
import { FaAngleDown } from 'react-icons/lib/fa' import { FaAngleDown } from 'react-icons/lib/fa'
import { btc } from 'utils'
import LoadingBolt from 'components/LoadingBolt' import LoadingBolt from 'components/LoadingBolt'
import CurrencyIcon from 'components/CurrencyIcon' import CurrencyIcon from 'components/CurrencyIcon'
@ -29,10 +30,11 @@ class Pay extends Component {
render() { render() {
const { const {
payform: { amount, payInput, showErrors, invoice }, payform: { amount, payInput, showErrors, invoice, showCurrencyFilters },
currency, currency,
crypto, crypto,
nodes, nodes,
ticker,
isOnchain, isOnchain,
isLn, isLn,
@ -41,6 +43,9 @@ class Pay extends Component {
inputCaption, inputCaption,
showPayLoadingScreen, showPayLoadingScreen,
payFormIsValid: { errors, isValid }, payFormIsValid: { errors, isValid },
payInputMin,
currentCurrencyFilters,
currencyName,
setPayAmount, setPayAmount,
onPayAmountBlur, onPayAmountBlur,
@ -48,20 +53,31 @@ class Pay extends Component {
setPayInput, setPayInput,
onPayInputBlur, onPayInputBlur,
onPaySubmit setCurrencyFilters,
onPaySubmit,
setCurrency
} = this.props } = this.props
const displayNodeName = (pubkey) => { const displayNodeName = (pubkey) => {
console.log('nodes: ', nodes)
console.log('pubkey: ', pubkey)
const node = find(nodes, n => n.pub_key === pubkey) const node = find(nodes, n => n.pub_key === pubkey)
console.log('node: ', node)
if (node && node.alias.length) { return node.alias } if (node && node.alias.length) { return node.alias }
return pubkey.substring(0, 10) return pubkey.substring(0, 10)
} }
const onCurrencyFilterClick = (currency) => {
if (!isLn) {
// change the input amount
setPayAmount(btc.convert(ticker.currency, currency, currentAmount))
}
setCurrency(currency)
setCurrencyFilters(false)
}
return ( return (
<div className={styles.container}> <div className={styles.container}>
{showPayLoadingScreen && <LoadingBolt />} {showPayLoadingScreen && <LoadingBolt />}
@ -122,18 +138,21 @@ class Pay extends Component {
readOnly={isLn} readOnly={isLn}
/> />
<div className={styles.currency}> <div className={styles.currency}>
<section className={styles.currentCurrency}> <section className={styles.currentCurrency} onClick={() => setCurrencyFilters(!showCurrencyFilters)}>
<span>BTC</span><span><FaAngleDown /></span> <span>{currencyName}</span><span><FaAngleDown /></span>
</section> </section>
<ul> <ul className={showCurrencyFilters && styles.active}>
<li>Bits</li> {
<li>Satoshis</li> currentCurrencyFilters.map(filter =>
<li key={filter.key} onClick={() => onCurrencyFilterClick(filter.key)}>{filter.name}</li>
)
}
</ul> </ul>
</div> </div>
</div> </div>
<div className={styles.usdAmount}> <div className={styles.usdAmount}>
{`${usdAmount} USD`} {`${usdAmount || 0} USD`}
</div> </div>
</section> </section>
@ -149,7 +168,10 @@ class Pay extends Component {
Pay.propTypes = { Pay.propTypes = {
payform: PropTypes.shape({ payform: PropTypes.shape({
amount: PropTypes.string.isRequired, amount: PropTypes.oneOfType([
PropTypes.string,
PropTypes.number
]).isRequired,
payInput: PropTypes.string.isRequired, payInput: PropTypes.string.isRequired,
showErrors: PropTypes.object.isRequired showErrors: PropTypes.object.isRequired
}).isRequired, }).isRequired,

18
app/components/Form/Pay.scss

@ -89,6 +89,7 @@
} }
.currency { .currency {
position: relative;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
@ -114,6 +115,23 @@
ul { ul {
visibility: hidden; visibility: hidden;
position: absolute; position: absolute;
top: 30px;
&.active {
visibility: visible;
}
li {
padding: 8px 15px;
background: #191919;
cursor: pointer;
transition: 0.25s hover;
border-bottom: 1px solid #0f0f0f;
&:hover {
background: #0f0f0f;
}
}
} }
} }

55
app/reducers/payform.js

@ -9,7 +9,7 @@ import { btc, bech32 } from '../utils'
// Initial State // Initial State
const initialState = { const initialState = {
amount: '0', amount: '',
payInput: '', payInput: '',
invoice: { invoice: {
@ -20,6 +20,8 @@ const initialState = {
destination: '' destination: ''
}, },
showCurrencyFilters: false,
showErrors: { showErrors: {
amount: false, amount: false,
payInput: false payInput: false
@ -32,6 +34,8 @@ export const SET_PAY_AMOUNT = 'SET_PAY_AMOUNT'
export const SET_PAY_INPUT = 'SET_PAY_INPUT' export const SET_PAY_INPUT = 'SET_PAY_INPUT'
export const SET_PAY_INVOICE = 'SET_PAY_INVOICE' export const SET_PAY_INVOICE = 'SET_PAY_INVOICE'
export const SET_CURRENCY_FILTERS = 'SET_CURRENCY_FILTERS'
export const UPDATE_PAY_ERRORS = 'UPDATE_PAY_ERRORS' export const UPDATE_PAY_ERRORS = 'UPDATE_PAY_ERRORS'
export const RESET_FORM = 'RESET_FORM' export const RESET_FORM = 'RESET_FORM'
@ -60,6 +64,13 @@ export function setPayInvoice(invoice) {
} }
} }
export function setCurrencyFilters(showCurrencyFilters) {
return {
type: SET_CURRENCY_FILTERS,
showCurrencyFilters
}
}
export function updatePayErrors(errorsObject) { export function updatePayErrors(errorsObject) {
return { return {
type: UPDATE_PAY_ERRORS, type: UPDATE_PAY_ERRORS,
@ -87,6 +98,7 @@ const ACTION_HANDLERS = {
[SET_PAY_AMOUNT]: (state, { amount }) => ({ ...state, amount, showErrors: Object.assign(state.showErrors, { amount: false }) }), [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_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 }) }), [SET_PAY_INVOICE]: (state, { invoice }) => ({ ...state, invoice, showErrors: Object.assign(state.showErrors, { amount: false }) }),
[SET_CURRENCY_FILTERS]: (state, { showCurrencyFilters }) => ({ ...state, showCurrencyFilters }),
[UPDATE_PAY_ERRORS]: (state, { errorsObject }) => ({ ...state, showErrors: Object.assign(state.showErrors, errorsObject) }), [UPDATE_PAY_ERRORS]: (state, { errorsObject }) => ({ ...state, showErrors: Object.assign(state.showErrors, errorsObject) }),
@ -140,12 +152,23 @@ payFormSelectors.currentAmount = createSelector(
payFormSelectors.isLn, payFormSelectors.isLn,
payAmountSelector, payAmountSelector,
payInvoiceSelector, payInvoiceSelector,
(isLn, amount, invoice) => { currencySelector,
(isLn, amount, invoice, currency) => {
if (isLn) { if (isLn) {
switch (currency) {
case 'btc':
return btc.satoshisToBtc((invoice.num_satoshis || 0)) 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 > 0 ? amount : null return amount
} }
) )
@ -162,7 +185,33 @@ payFormSelectors.usdAmount = createSelector(
return btc.satoshisToUsd((invoice.num_satoshis || 0), ticker.price_usd) return btc.satoshisToUsd((invoice.num_satoshis || 0), ticker.price_usd)
} }
switch (currency) {
case 'btc':
return btc.btcToUsd(amount, ticker.price_usd) return btc.btcToUsd(amount, ticker.price_usd)
case 'bits':
return btc.bitsToUsd(amount, ticker.price_usd)
case 'sats':
return btc.satoshisToUsd(amount, ticker.price_usd)
default:
return ''
}
}
)
payFormSelectors.payInputMin = createSelector(
currencySelector,
(currency) => {
switch (currency) {
case 'btc':
return '0.00000001'
case 'bits':
return '0.01'
case 'sats':
return '1'
default:
return '0'
}
} }
) )

34
app/reducers/ticker.js

@ -77,6 +77,8 @@ const ACTION_HANDLERS = {
// Selectors // Selectors
const tickerSelectors = {} const tickerSelectors = {}
const cryptoSelector = state => state.ticker.crypto const cryptoSelector = state => state.ticker.crypto
const currencyFiltersSelector = state => state.ticker.currencyFilters
const currencySelector = state => state.ticker.currency
const bitcoinTickerSelector = state => state.ticker.btcTicker const bitcoinTickerSelector = state => state.ticker.btcTicker
const litecoinTickerSelector = state => state.ticker.ltcTicker const litecoinTickerSelector = state => state.ticker.ltcTicker
@ -87,6 +89,22 @@ tickerSelectors.currentTicker = createSelector(
(crypto, btcTicker, ltcTicker) => (crypto === 'btc' ? btcTicker : ltcTicker) (crypto, btcTicker, ltcTicker) => (crypto === 'btc' ? btcTicker : ltcTicker)
) )
tickerSelectors.currentCurrencyFilters = createSelector(
currencySelector,
currencyFiltersSelector,
(currency, filters) => filters.filter(f => f.key !== currency)
)
tickerSelectors.currencyName = createSelector(
currencySelector,
(currency) => {
if (currency === 'btc') { return 'BTC' }
if (currency === 'sats') { return 'satohis' }
return currency
}
)
export { tickerSelectors } export { tickerSelectors }
// ------------------------------------ // ------------------------------------
@ -97,7 +115,21 @@ const initialState = {
currency: '', currency: '',
crypto: '', crypto: '',
btcTicker: null, btcTicker: null,
ltcTicker: null ltcTicker: null,
currencyFilters: [
{
key: 'btc',
name: 'BTC'
},
{
key: 'bits',
name: 'bits'
},
{
key: 'sats',
name: 'satoshis'
}
]
} }
export default function tickerReducer(state = initialState, action) { export default function tickerReducer(state = initialState, action) {

12
app/routes/app/containers/AppContainer.js

@ -11,7 +11,7 @@ import { showModal, hideModal } from 'reducers/modal'
import { setFormType } from 'reducers/form' import { setFormType } from 'reducers/form'
import { setPayAmount, setPayInput, updatePayErrors, payFormSelectors } from 'reducers/payform' import { setPayAmount, setPayInput, setCurrencyFilters, updatePayErrors, payFormSelectors } from 'reducers/payform'
import { setRequestAmount, setRequestMemo } from 'reducers/requestform' import { setRequestAmount, setRequestMemo } from 'reducers/requestform'
@ -69,6 +69,7 @@ const mapDispatchToProps = {
setPayAmount, setPayAmount,
setPayInput, setPayInput,
setCurrencyFilters,
updatePayErrors, updatePayErrors,
setRequestAmount, setRequestAmount,
@ -130,6 +131,8 @@ const mapStateToProps = state => ({
network: state.network, network: state.network,
currentTicker: tickerSelectors.currentTicker(state), currentTicker: tickerSelectors.currentTicker(state),
currentCurrencyFilters: tickerSelectors.currentCurrencyFilters(state),
currencyName: tickerSelectors.currencyName(state),
isOnchain: payFormSelectors.isOnchain(state), isOnchain: payFormSelectors.isOnchain(state),
isLn: payFormSelectors.isLn(state), isLn: payFormSelectors.isLn(state),
currentAmount: payFormSelectors.currentAmount(state), currentAmount: payFormSelectors.currentAmount(state),
@ -137,6 +140,7 @@ const mapStateToProps = state => ({
inputCaption: payFormSelectors.inputCaption(state), inputCaption: payFormSelectors.inputCaption(state),
showPayLoadingScreen: payFormSelectors.showPayLoadingScreen(state), showPayLoadingScreen: payFormSelectors.showPayLoadingScreen(state),
payFormIsValid: payFormSelectors.payFormIsValid(state), payFormIsValid: payFormSelectors.payFormIsValid(state),
payInputMin: payFormSelectors.payInputMin(state),
syncPercentage: lndSelectors.syncPercentage(state), syncPercentage: lndSelectors.syncPercentage(state),
filteredNetworkNodes: contactFormSelectors.filteredNetworkNodes(state), filteredNetworkNodes: contactFormSelectors.filteredNetworkNodes(state),
@ -157,6 +161,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
currency: stateProps.ticker.currency, currency: stateProps.ticker.currency,
crypto: stateProps.ticker.crypto, crypto: stateProps.ticker.crypto,
nodes: stateProps.network.nodes, nodes: stateProps.network.nodes,
ticker: stateProps.ticker,
isOnchain: stateProps.isOnchain, isOnchain: stateProps.isOnchain,
isLn: stateProps.isLn, isLn: stateProps.isLn,
@ -165,10 +170,15 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => {
inputCaption: stateProps.inputCaption, inputCaption: stateProps.inputCaption,
showPayLoadingScreen: stateProps.showPayLoadingScreen, showPayLoadingScreen: stateProps.showPayLoadingScreen,
payFormIsValid: stateProps.payFormIsValid, payFormIsValid: stateProps.payFormIsValid,
payInputMin: stateProps.payInputMin,
currentCurrencyFilters: stateProps.currentCurrencyFilters,
currencyName: stateProps.currencyName,
setPayAmount: dispatchProps.setPayAmount, setPayAmount: dispatchProps.setPayAmount,
setPayInput: dispatchProps.setPayInput, setPayInput: dispatchProps.setPayInput,
setCurrencyFilters: dispatchProps.setCurrencyFilters,
fetchInvoice: dispatchProps.fetchInvoice, fetchInvoice: dispatchProps.fetchInvoice,
setCurrency: dispatchProps.setCurrency,
onPayAmountBlur: () => { onPayAmountBlur: () => {
// If the amount is now valid and showErrors was on, turn it off // If the amount is now valid and showErrors was on, turn it off

99
app/utils/btc.js

@ -1,11 +1,52 @@
import sb from 'satoshi-bitcoin' import sb from 'satoshi-bitcoin'
//////////////////////
// BTC to things /////
/////////////////////
export function btcToSatoshis(btc) { export function btcToSatoshis(btc) {
if (btc === undefined || btc === null || btc === '') return null if (btc === undefined || btc === null || btc === '') return null
return sb.toSatoshi(btc) return sb.toSatoshi(btc)
} }
export function btcToBits(btc) {
if (btc === undefined || btc === null || btc === '') return null
return satoshisToBits(sb.toSatoshi(btc))
}
export function btcToUsd(btc, price) {
const amount = parseFloat(btc * price).toFixed(2)
return (btc > 0 && amount <= 0) ? '< 0.01' : amount.toLocaleString('en')
}
////////////////////////////
// bits to things /////////
//////////////////////////
export function bitsToBtc(bits, price) {
if (bits === undefined || bits === null || bits === '') return null
const sats = bits * 100
return satoshisToBtc(sats, price)
}
export function bitsToSatoshis(bits, price) {
if (bits === undefined || bits === null || bits === '') return null
return bits * 100
}
export function bitsToUsd(bits, price) {
if (bits === undefined || bits === null || bits === '') return null
const sats = bits * 100
return satoshisToUsd(sats, price)
}
////////////////////////////
// satoshis to things /////
//////////////////////////
export function satoshisToBtc(satoshis) { export function satoshisToBtc(satoshis) {
if (satoshis === undefined || satoshis === null || satoshis === '') return null if (satoshis === undefined || satoshis === null || satoshis === '') return null
@ -20,17 +61,13 @@ export function satoshisToBits(satoshis) {
return bitsAmount > 0 ? bitsAmount : bitsAmount * -1 return bitsAmount > 0 ? bitsAmount : bitsAmount * -1
} }
export function btcToUsd(btc, price) {
const amount = parseFloat(btc * price).toFixed(2)
return (btc > 0 && amount <= 0) ? '< 0.01' : amount.toLocaleString('en')
}
export function satoshisToUsd(satoshis, price) { export function satoshisToUsd(satoshis, price) {
if (satoshis === undefined || satoshis === null || satoshis === '') return null if (satoshis === undefined || satoshis === null || satoshis === '') return null
return btcToUsd(satoshisToBtc(satoshis), price) return btcToUsd(satoshisToBtc(satoshis), price)
} }
export function renderCurrency(currency) { export function renderCurrency(currency) {
switch (currency) { switch (currency) {
case 'btc': case 'btc':
@ -44,11 +81,57 @@ export function renderCurrency(currency) {
} }
} }
export function convert(from, to, amount, price) {
switch (from) {
case 'btc':
switch (to) {
case 'bits':
return btcToBits(amount)
case 'sats':
return btcToSatoshis(amount)
case 'usd':
return btcToUsd(amount, price)
}
break
case 'bits':
switch (to) {
case 'btc':
return bitsToBtc(amount)
case 'sats':
return bitsToSatoshis(amount)
case 'usd':
return bitsToUsd(amount, price)
}
break
case 'sats':
switch (to) {
case 'btc':
return satoshisToBtc(amount)
case 'bits':
return satoshisToBits(amount)
case 'usd':
return satoshisToUsd(amount, price)
}
break
default:
return ''
}
}
export default { export default {
btcToSatoshis, btcToSatoshis,
btcToBits,
btcToUsd,
bitsToBtc,
bitsToSatoshis,
bitsToUsd,
satoshisToBtc, satoshisToBtc,
satoshisToBits, satoshisToBits,
satoshisToUsd, satoshisToUsd,
btcToUsd,
renderCurrency renderCurrency,
convert
} }

Loading…
Cancel
Save