jackmallers
7 years ago
committed by
GitHub
34 changed files with 960 additions and 817 deletions
@ -0,0 +1,42 @@ |
|||
import React from 'react' |
|||
import PropTypes from 'prop-types' |
|||
|
|||
import { MdClose } from 'react-icons/lib/md' |
|||
|
|||
import PayForm from './PayForm' |
|||
import RequestForm from './RequestForm' |
|||
|
|||
import styles from './Form.scss' |
|||
|
|||
const FORM_TYPES = { |
|||
PAY_FORM: PayForm, |
|||
REQUEST_FORM: RequestForm |
|||
} |
|||
|
|||
const Form = ({ formType, formProps, closeForm }) => { |
|||
if (!formType) { return null } |
|||
|
|||
const FormComponent = FORM_TYPES[formType] |
|||
return ( |
|||
<div className={`${styles.outtercontainer} ${formType && styles.open}`}> |
|||
<div className={styles.innercontainer}> |
|||
<div className={styles.esc} onClick={closeForm}> |
|||
<MdClose /> |
|||
</div> |
|||
|
|||
<div className={styles.content}> |
|||
<FormComponent {...formProps} /> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
|
|||
Form.propTypes = { |
|||
formType: PropTypes.string, |
|||
formProps: PropTypes.object.isRequired, |
|||
closeForm: PropTypes.func.isRequired |
|||
} |
|||
|
|||
export default Form |
@ -0,0 +1,59 @@ |
|||
@import '../../variables.scss'; |
|||
|
|||
.outtercontainer { |
|||
position: absolute; |
|||
top: 0; |
|||
bottom: 0; |
|||
width: 100%; |
|||
height: 100vh; |
|||
background: $white; |
|||
z-index: 0; |
|||
opacity: 0; |
|||
transition: all 0.5s; |
|||
|
|||
&.open { |
|||
opacity: 1; |
|||
z-index: 10; |
|||
} |
|||
} |
|||
|
|||
.innercontainer { |
|||
position: relative; |
|||
height: 100vh; |
|||
margin: 5%; |
|||
} |
|||
|
|||
.esc { |
|||
position: absolute; |
|||
top: 0; |
|||
right: 0; |
|||
color: $darkestgrey; |
|||
cursor: pointer; |
|||
padding: 20px; |
|||
border-radius: 50%; |
|||
|
|||
&:hover { |
|||
color: $bluegrey; |
|||
background: $darkgrey; |
|||
} |
|||
|
|||
&:active { |
|||
color: $white; |
|||
background: $main; |
|||
} |
|||
|
|||
svg { |
|||
width: 32px; |
|||
height: 32px; |
|||
} |
|||
} |
|||
|
|||
.content { |
|||
width: 50%; |
|||
margin: 0 auto; |
|||
display: flex; |
|||
flex-direction: column; |
|||
height: 75vh; |
|||
justify-content: center; |
|||
align-items: center; |
|||
} |
@ -0,0 +1,123 @@ |
|||
import React, { Component } from 'react' |
|||
import PropTypes from 'prop-types' |
|||
|
|||
import { FaBolt, FaChain } from 'react-icons/lib/fa' |
|||
import LoadingBolt from 'components/LoadingBolt' |
|||
import CurrencyIcon from 'components/CurrencyIcon' |
|||
|
|||
import styles from './PayForm.scss' |
|||
|
|||
class PayForm extends Component { |
|||
componentDidUpdate(prevProps) { |
|||
const { isOnchain, isLn, payform: { payInput }, fetchInvoice } = this.props |
|||
|
|||
// If on-chain, focus on amount to let user know it's editable
|
|||
if (isOnchain) { this.amountInput.focus() } |
|||
// If LN go retrieve invoice details
|
|||
if ((prevProps.payform.payInput !== payInput) && isLn) { |
|||
fetchInvoice(payInput) |
|||
} |
|||
} |
|||
|
|||
render() { |
|||
const { |
|||
payform: { amount, payInput }, |
|||
currency, |
|||
crypto, |
|||
|
|||
isOnchain, |
|||
isLn, |
|||
currentAmount, |
|||
inputCaption, |
|||
showPayLoadingScreen, |
|||
|
|||
setPayAmount, |
|||
setPayInput, |
|||
|
|||
onPaySubmit |
|||
} = this.props |
|||
|
|||
return ( |
|||
<div className={styles.container}> |
|||
{showPayLoadingScreen && <LoadingBolt />} |
|||
|
|||
<section className={`${styles.amountContainer} ${isLn ? styles.ln : ''}`}> |
|||
<label htmlFor='amount'> |
|||
<CurrencyIcon currency={currency} crypto={crypto} /> |
|||
</label> |
|||
<input |
|||
type='number' |
|||
min='0' |
|||
ref={input => this.amountInput = input} // eslint-disable-line
|
|||
size='' |
|||
style={ |
|||
isLn ? |
|||
{ width: '75%', fontSize: '85px' } |
|||
: |
|||
{ width: `${amount.length > 1 ? (amount.length * 15) - 5 : 25}%`, fontSize: `${190 - (amount.length ** 2)}px` } |
|||
} |
|||
value={currentAmount} |
|||
onChange={event => setPayAmount(event.target.value)} |
|||
id='amount' |
|||
readOnly={isLn} |
|||
/> |
|||
</section> |
|||
<div className={styles.inputContainer}> |
|||
<div className={styles.info}> |
|||
<span>{inputCaption}</span> |
|||
</div> |
|||
<aside className={styles.paymentIcon}> |
|||
{isOnchain && |
|||
<i> |
|||
<span>on-chain</span> |
|||
<FaChain /> |
|||
</i> |
|||
} |
|||
{isLn && |
|||
<i> |
|||
<span>lightning network</span> |
|||
<FaBolt /> |
|||
</i> |
|||
} |
|||
</aside> |
|||
<section className={styles.input}> |
|||
<input |
|||
type='text' |
|||
placeholder='Payment request or bitcoin address' |
|||
value={payInput} |
|||
onChange={event => setPayInput(event.target.value)} |
|||
id='paymentRequest' |
|||
/> |
|||
</section> |
|||
</div> |
|||
<section className={styles.buttonGroup}> |
|||
<div className={styles.button} onClick={onPaySubmit}>Pay</div> |
|||
</section> |
|||
</div> |
|||
) |
|||
} |
|||
} |
|||
|
|||
|
|||
PayForm.propTypes = { |
|||
payform: PropTypes.object.isRequired, |
|||
currency: PropTypes.string.isRequired, |
|||
crypto: PropTypes.string.isRequired, |
|||
|
|||
isOnchain: PropTypes.bool.isRequired, |
|||
isLn: PropTypes.bool.isRequired, |
|||
currentAmount: PropTypes.oneOfType([ |
|||
PropTypes.string, |
|||
PropTypes.number |
|||
]).isRequired, |
|||
inputCaption: PropTypes.string.isRequired, |
|||
showPayLoadingScreen: PropTypes.bool.isRequired, |
|||
|
|||
setPayAmount: PropTypes.func.isRequired, |
|||
setPayInput: PropTypes.func.isRequired, |
|||
fetchInvoice: PropTypes.func.isRequired, |
|||
|
|||
onPaySubmit: PropTypes.func.isRequired |
|||
} |
|||
|
|||
export default PayForm |
@ -0,0 +1,59 @@ |
|||
import React from 'react' |
|||
import PropTypes from 'prop-types' |
|||
import CurrencyIcon from 'components/CurrencyIcon' |
|||
import styles from './RequestForm.scss' |
|||
|
|||
const RequestForm = ({ |
|||
requestform: { amount, memo }, |
|||
currency, |
|||
crypto, |
|||
|
|||
setRequestAmount, |
|||
setRequestMemo, |
|||
|
|||
onRequestSubmit |
|||
}) => ( |
|||
<div className={styles.container}> |
|||
<section className={styles.amountContainer}> |
|||
<label htmlFor='amount'> |
|||
<CurrencyIcon currency={currency} crypto={crypto} /> |
|||
</label> |
|||
<input |
|||
type='text' |
|||
size='' |
|||
style={{ width: `${amount.length > 1 ? (amount.length * 15) - 5 : 25}%`, fontSize: `${190 - (amount.length ** 2)}px` }} |
|||
value={amount} |
|||
onChange={event => setRequestAmount(event.target.value)} |
|||
id='amount' |
|||
/> |
|||
</section> |
|||
<section className={styles.inputContainer}> |
|||
<label htmlFor='memo'>Request:</label> |
|||
<input |
|||
type='text' |
|||
placeholder='Dinner, Rent, etc' |
|||
value={memo} |
|||
onChange={event => setRequestMemo(event.target.value)} |
|||
id='memo' |
|||
/> |
|||
</section> |
|||
<section className={styles.buttonGroup}> |
|||
<div className={styles.button} onClick={onRequestSubmit}> |
|||
Request |
|||
</div> |
|||
</section> |
|||
</div> |
|||
) |
|||
|
|||
RequestForm.propTypes = { |
|||
requestform: PropTypes.object.isRequired, |
|||
currency: PropTypes.string.isRequired, |
|||
crypto: PropTypes.string.isRequired, |
|||
|
|||
setRequestAmount: PropTypes.func.isRequired, |
|||
setRequestMemo: PropTypes.func.isRequired, |
|||
|
|||
onRequestSubmit: PropTypes.func.isRequired |
|||
} |
|||
|
|||
export default RequestForm |
@ -1,4 +1,4 @@ |
|||
@import '../../../../../../../variables.scss'; |
|||
@import '../../variables.scss'; |
|||
|
|||
.container { |
|||
margin: 0 auto; |
@ -0,0 +1,161 @@ |
|||
import { createSelector } from 'reselect' |
|||
import bitcoin from 'bitcoinjs-lib' |
|||
import { tickerSelectors } from './ticker' |
|||
import { btc, bech32 } from '../utils' |
|||
|
|||
// Initial State
|
|||
const initialState = { |
|||
amount: '0', |
|||
payInput: '', |
|||
|
|||
invoice: { |
|||
payreq: '', |
|||
r_hash: '', |
|||
amount: '0' |
|||
} |
|||
} |
|||
|
|||
// 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 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 resetPayForm() { |
|||
return { |
|||
type: RESET_FORM |
|||
} |
|||
} |
|||
|
|||
// ------------------------------------
|
|||
// Action Handlers
|
|||
// ------------------------------------
|
|||
const ACTION_HANDLERS = { |
|||
[SET_PAY_AMOUNT]: (state, { amount }) => ({ ...state, amount }), |
|||
[SET_PAY_INPUT]: (state, { payInput }) => ({ ...state, payInput }), |
|||
[SET_PAY_INVOICE]: (state, { invoice }) => ({ ...state, invoice }), |
|||
|
|||
[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 |
|||
|
|||
// transaction
|
|||
const sendingPaymentSelector = state => state.payment.sendingPayment |
|||
|
|||
// ticker
|
|||
const currencySelector = state => state.ticker.currency |
|||
|
|||
payFormSelectors.isOnchain = createSelector( |
|||
payInputSelector, |
|||
(input) => { |
|||
// TODO: work with bitcoin-js to fix p2wkh error and make testnet/mainnet dynamic
|
|||
try { |
|||
bitcoin.address.toOutputScript(input, bitcoin.networks.testnet) |
|||
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, |
|||
tickerSelectors.currentTicker, |
|||
(isLn, amount, invoice, currency, ticker) => { |
|||
if (isLn) { |
|||
return currency === 'usd' ? btc.satoshisToUsd((invoice.num_satoshis || 0), ticker.price_usd) : btc.satoshisToBtc((invoice.num_satoshis || 0)) |
|||
} |
|||
|
|||
return amount |
|||
} |
|||
) |
|||
|
|||
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 |
|||
) |
|||
|
|||
export { payFormSelectors } |
|||
|
|||
// ------------------------------------
|
|||
// Reducer
|
|||
// ------------------------------------
|
|||
export default function payFormReducer(state = initialState, action) { |
|||
const handler = ACTION_HANDLERS[action.type] |
|||
|
|||
return handler ? handler(state, action) : state |
|||
} |
@ -0,0 +1,55 @@ |
|||
// Initial State
|
|||
const initialState = { |
|||
amount: '0', |
|||
memo: '' |
|||
} |
|||
|
|||
// Constants
|
|||
// ------------------------------------
|
|||
export const SET_REQUEST_AMOUNT = 'SET_REQUEST_AMOUNT' |
|||
export const SET_REQUEST_MEMO = 'SET_REQUEST_MEMO' |
|||
export const SET_PAY_INVOICE = 'SET_PAY_INVOICE' |
|||
|
|||
export const RESET_FORM = 'RESET_FORM' |
|||
|
|||
// ------------------------------------
|
|||
// Actions
|
|||
// ------------------------------------
|
|||
export function setRequestAmount(amount) { |
|||
return { |
|||
type: SET_REQUEST_AMOUNT, |
|||
amount |
|||
} |
|||
} |
|||
|
|||
export function setRequestMemo(memo) { |
|||
return { |
|||
type: SET_REQUEST_MEMO, |
|||
memo |
|||
} |
|||
} |
|||
|
|||
export function resetRequestForm() { |
|||
return { |
|||
type: RESET_FORM |
|||
} |
|||
} |
|||
|
|||
// ------------------------------------
|
|||
// Action Handlers
|
|||
// ------------------------------------
|
|||
const ACTION_HANDLERS = { |
|||
[SET_REQUEST_AMOUNT]: (state, { amount }) => ({ ...state, amount }), |
|||
[SET_REQUEST_MEMO]: (state, { memo }) => ({ ...state, memo }), |
|||
|
|||
[RESET_FORM]: () => (initialState) |
|||
} |
|||
|
|||
// ------------------------------------
|
|||
// Reducer
|
|||
// ------------------------------------
|
|||
export default function payFormReducer(state = initialState, action) { |
|||
const handler = ACTION_HANDLERS[action.type] |
|||
|
|||
return handler ? handler(state, action) : state |
|||
} |
@ -1,93 +0,0 @@ |
|||
import React from 'react' |
|||
import PropTypes from 'prop-types' |
|||
import { MdClose } from 'react-icons/lib/md' |
|||
import Pay from './components/Pay' |
|||
import Request from './components/Request' |
|||
import styles from './Form.scss' |
|||
|
|||
const Form = ({ |
|||
form: { formType, amount, onchainAmount, message, payment_request }, |
|||
setAmount, |
|||
setOnchainAmount, |
|||
setMessage, |
|||
setPaymentRequest, |
|||
ticker: { currency, crypto }, |
|||
isOpen, |
|||
close, |
|||
createInvoice, |
|||
payInvoice, |
|||
sendCoins, |
|||
fetchInvoice, |
|||
formInvoice, |
|||
currentTicker, |
|||
isOnchain, |
|||
isLn, |
|||
sendingTransaction |
|||
}) => ( |
|||
<div className={`${styles.formContainer} ${isOpen ? styles.open : ''}`}> |
|||
<div className={styles.container}> |
|||
<div className={styles.esc} onClick={close}> |
|||
<MdClose /> |
|||
</div> |
|||
<div className={styles.content}> |
|||
{ |
|||
formType === 'pay' ? |
|||
<Pay |
|||
sendingTransaction={sendingTransaction} |
|||
invoiceAmount={formInvoice.amount} |
|||
onchainAmount={onchainAmount} |
|||
setOnchainAmount={setOnchainAmount} |
|||
amount={formInvoice.amount} |
|||
payment_request={payment_request} |
|||
setPaymentRequest={setPaymentRequest} |
|||
fetchInvoice={fetchInvoice} |
|||
payInvoice={payInvoice} |
|||
sendCoins={sendCoins} |
|||
currentTicker={currentTicker} |
|||
currency={currency} |
|||
crypto={crypto} |
|||
close={close} |
|||
isOnchain={isOnchain} |
|||
isLn={isLn} |
|||
/> |
|||
: |
|||
<Request |
|||
amount={amount} |
|||
setAmount={setAmount} |
|||
payment_request={payment_request} |
|||
setMessage={setMessage} |
|||
createInvoice={createInvoice} |
|||
message={message} |
|||
currentTicker={currentTicker} |
|||
currency={currency} |
|||
crypto={crypto} |
|||
close={close} |
|||
/> |
|||
|
|||
} |
|||
</div> |
|||
</div> |
|||
</div> |
|||
) |
|||
|
|||
Form.propTypes = { |
|||
form: PropTypes.object.isRequired, |
|||
ticker: PropTypes.object.isRequired, |
|||
setAmount: PropTypes.func.isRequired, |
|||
setOnchainAmount: PropTypes.func.isRequired, |
|||
setMessage: PropTypes.func.isRequired, |
|||
setPaymentRequest: PropTypes.func.isRequired, |
|||
isOpen: PropTypes.bool.isRequired, |
|||
close: PropTypes.func.isRequired, |
|||
createInvoice: PropTypes.func.isRequired, |
|||
payInvoice: PropTypes.func.isRequired, |
|||
sendCoins: PropTypes.func.isRequired, |
|||
fetchInvoice: PropTypes.func.isRequired, |
|||
formInvoice: PropTypes.object.isRequired, |
|||
currentTicker: PropTypes.object.isRequired, |
|||
isOnchain: PropTypes.bool.isRequired, |
|||
isLn: PropTypes.bool.isRequired, |
|||
sendingTransaction: PropTypes.bool.isRequired |
|||
} |
|||
|
|||
export default Form |
@ -1,159 +0,0 @@ |
|||
@import '../../../../../variables.scss'; |
|||
|
|||
.formContainer { |
|||
position: absolute; |
|||
top: 0; |
|||
bottom: 0; |
|||
width: 100%; |
|||
height: 100vh; |
|||
background: $white; |
|||
z-index: 0; |
|||
opacity: 0; |
|||
transition: all 0.5s; |
|||
|
|||
&.open { |
|||
opacity: 1; |
|||
z-index: 10; |
|||
} |
|||
} |
|||
|
|||
.container { |
|||
position: relative; |
|||
height: 100vh; |
|||
margin: 5%; |
|||
} |
|||
|
|||
.esc { |
|||
position: absolute; |
|||
top: 0; |
|||
right: 0; |
|||
color: $darkestgrey; |
|||
cursor: pointer; |
|||
padding: 20px; |
|||
border-radius: 50%; |
|||
|
|||
&:hover { |
|||
color: $bluegrey; |
|||
background: $darkgrey; |
|||
} |
|||
|
|||
&:active { |
|||
color: $white; |
|||
background: $main; |
|||
} |
|||
|
|||
svg { |
|||
width: 32px; |
|||
height: 32px; |
|||
} |
|||
} |
|||
|
|||
.content { |
|||
width: 50%; |
|||
margin: 0 auto; |
|||
display: flex; |
|||
flex-direction: column; |
|||
height: 75vh; |
|||
justify-content: center; |
|||
align-items: center; |
|||
|
|||
.amountContainer { |
|||
color: $main; |
|||
display: flex; |
|||
justify-content: center; |
|||
min-height: 120px; |
|||
margin-bottom: 20px; |
|||
|
|||
label, input[type=text] { |
|||
color: inherit; |
|||
display: inline-block; |
|||
vertical-align: top; |
|||
padding: 0; |
|||
} |
|||
|
|||
label { |
|||
svg { |
|||
width: 85px; |
|||
height: 85px; |
|||
} |
|||
|
|||
svg[data-icon='ltc'] { |
|||
margin-right: 10px; |
|||
|
|||
g { |
|||
transform: scale(1.75) translate(-5px, -5px); |
|||
} |
|||
} |
|||
} |
|||
|
|||
input[type=text] { |
|||
width: 100px; |
|||
font-size: 180px; |
|||
border: none; |
|||
outline: 0; |
|||
-webkit-appearance: none; |
|||
} |
|||
} |
|||
|
|||
.inputContainer { |
|||
width: 100%; |
|||
display: flex; |
|||
justify-content: center; |
|||
font-size: 18px; |
|||
height: auto; |
|||
min-height: 55px; |
|||
margin-bottom: 20px; |
|||
border: 1px solid $traditionalgrey; |
|||
border-radius: 6px; |
|||
position: relative; |
|||
padding: 0 20px; |
|||
|
|||
label, input[type=text] { |
|||
font-size: inherit; |
|||
} |
|||
|
|||
label { |
|||
padding-top: 19px; |
|||
padding-bottom: 12px; |
|||
color: $traditionalgrey; |
|||
} |
|||
|
|||
input[type=text] { |
|||
width: 100%; |
|||
border: none; |
|||
outline: 0; |
|||
-webkit-appearance: none; |
|||
height: 55px; |
|||
padding: 0 10px; |
|||
} |
|||
} |
|||
|
|||
.buttonGroup { |
|||
width: 100%; |
|||
display: flex; |
|||
flex-direction: row; |
|||
border-radius: 6px; |
|||
overflow: hidden; |
|||
|
|||
.button { |
|||
cursor: pointer; |
|||
height: 55px; |
|||
min-height: 55px; |
|||
text-transform: none; |
|||
font-size: 18px; |
|||
transition: opacity .2s ease-out; |
|||
background: $main; |
|||
color: $white; |
|||
border: none; |
|||
font-weight: 500; |
|||
padding: 0; |
|||
width: 100%; |
|||
text-align: center; |
|||
line-height: 55px; |
|||
|
|||
&:first-child { |
|||
border-right: 1px solid lighten($main, 20%); |
|||
} |
|||
} |
|||
} |
|||
} |
@ -1,149 +0,0 @@ |
|||
import React, { Component } from 'react' |
|||
import PropTypes from 'prop-types' |
|||
import { FaBolt, FaChain } from 'react-icons/lib/fa' |
|||
import CurrencyIcon from 'components/CurrencyIcon' |
|||
import LoadingBolt from 'components/LoadingBolt' |
|||
import { btc } from 'utils' |
|||
import styles from './Pay.scss' |
|||
|
|||
class Pay extends Component { |
|||
componentDidUpdate(prevProps) { |
|||
const { isOnchain, isLn, fetchInvoice, payment_request } = this.props |
|||
|
|||
if (isOnchain) { this.amountInput.focus() } |
|||
if ((prevProps.payment_request !== payment_request) && isLn) { fetchInvoice(payment_request) } |
|||
} |
|||
|
|||
render() { |
|||
const { |
|||
sendingTransaction, |
|||
invoiceAmount, |
|||
onchainAmount, |
|||
setOnchainAmount, |
|||
payment_request, |
|||
setPaymentRequest, |
|||
payInvoice, |
|||
sendCoins, |
|||
currentTicker, |
|||
currency, |
|||
crypto, |
|||
isOnchain, |
|||
isLn |
|||
} = this.props |
|||
|
|||
const payClicked = () => { |
|||
if (!isOnchain && !isLn) { return } |
|||
|
|||
if (isOnchain) { sendCoins({ value: onchainAmount, addr: payment_request, currency, rate: currentTicker.price_usd }) } |
|||
if (isLn) { payInvoice(payment_request) } |
|||
} |
|||
|
|||
const calculateAmount = value => (currency === 'usd' ? btc.satoshisToUsd(value, currentTicker.price_usd) : btc.satoshisToBtc(value)) |
|||
|
|||
return ( |
|||
<div> |
|||
{ |
|||
sendingTransaction ? |
|||
<LoadingBolt /> |
|||
: |
|||
null |
|||
|
|||
} |
|||
<div className={styles.container}> |
|||
<section className={`${styles.amountContainer} ${isLn ? styles.ln : ''}`}> |
|||
<label htmlFor='amount'> |
|||
<CurrencyIcon currency={currency} crypto={crypto} /> |
|||
</label> |
|||
<input |
|||
type='text' |
|||
ref={input => this.amountInput = input} // eslint-disable-line
|
|||
size='' |
|||
style={ |
|||
isLn ? |
|||
{ width: '75%', fontSize: '85px' } |
|||
: |
|||
{ width: `${onchainAmount.length > 1 ? (onchainAmount.length * 15) - 5 : 25}%`, fontSize: `${190 - (onchainAmount.length ** 2)}px` } |
|||
} |
|||
value={isLn ? calculateAmount(invoiceAmount) : onchainAmount} |
|||
onChange={event => setOnchainAmount(event.target.value)} |
|||
id='amount' |
|||
readOnly={isLn} |
|||
/> |
|||
</section> |
|||
<div className={styles.inputContainer}> |
|||
<div className={styles.info}> |
|||
{(() => { |
|||
if (isOnchain) { |
|||
return ( |
|||
<span>{`You're about to send ${onchainAmount} ${currency.toUpperCase()} on-chain which should take around 10 minutes`}</span> |
|||
) |
|||
} else if (isLn) { |
|||
return ( |
|||
<span>{`You're about to send ${calculateAmount(invoiceAmount)} ${currency.toUpperCase()} over the Lightning Network which will be instant`}</span> // eslint-disable-line |
|||
) |
|||
} |
|||
return null |
|||
})()} |
|||
</div> |
|||
<aside className={styles.paymentIcon}> |
|||
{(() => { |
|||
if (isOnchain) { |
|||
return ( |
|||
<i> |
|||
<span>on-chain</span> |
|||
<FaChain /> |
|||
</i> |
|||
) |
|||
} else if (isLn) { |
|||
return ( |
|||
<i> |
|||
<span>lightning network</span> |
|||
<FaBolt /> |
|||
</i> |
|||
) |
|||
} |
|||
return null |
|||
})()} |
|||
</aside> |
|||
<section className={styles.input}> |
|||
<input |
|||
type='text' |
|||
placeholder='Payment request or bitcoin address' |
|||
value={payment_request} |
|||
onChange={event => setPaymentRequest(event.target.value)} |
|||
id='paymentRequest' |
|||
/> |
|||
</section> |
|||
</div> |
|||
<section className={styles.buttonGroup}> |
|||
<div className={styles.button} onClick={payClicked}> |
|||
Pay |
|||
</div> |
|||
</section> |
|||
</div> |
|||
</div> |
|||
) |
|||
} |
|||
} |
|||
|
|||
Pay.propTypes = { |
|||
sendingTransaction: PropTypes.bool.isRequired, |
|||
invoiceAmount: PropTypes.oneOfType([ |
|||
PropTypes.string, |
|||
PropTypes.number |
|||
]).isRequired, |
|||
onchainAmount: PropTypes.string.isRequired, |
|||
setOnchainAmount: PropTypes.func.isRequired, |
|||
payment_request: PropTypes.string.isRequired, |
|||
setPaymentRequest: PropTypes.func.isRequired, |
|||
fetchInvoice: PropTypes.func.isRequired, |
|||
payInvoice: PropTypes.func.isRequired, |
|||
sendCoins: PropTypes.func.isRequired, |
|||
currentTicker: PropTypes.object.isRequired, |
|||
currency: PropTypes.string.isRequired, |
|||
crypto: PropTypes.string.isRequired, |
|||
isOnchain: PropTypes.bool.isRequired, |
|||
isLn: PropTypes.bool.isRequired |
|||
} |
|||
|
|||
export default Pay |
@ -1,3 +0,0 @@ |
|||
import Pay from './Pay' |
|||
|
|||
export default Pay |
@ -1,68 +0,0 @@ |
|||
import React from 'react' |
|||
import PropTypes from 'prop-types' |
|||
import CurrencyIcon from 'components/CurrencyIcon' |
|||
import styles from './Request.scss' |
|||
|
|||
const Request = ({ |
|||
amount, |
|||
setAmount, |
|||
setMessage, |
|||
createInvoice, |
|||
message, |
|||
currentTicker, |
|||
currency, |
|||
crypto, |
|||
close |
|||
}) => { |
|||
const requestClicked = () => { |
|||
createInvoice(amount, message, currency, currentTicker.price_usd) |
|||
close() |
|||
} |
|||
|
|||
return ( |
|||
<div className={styles.container}> |
|||
<section className={styles.amountContainer}> |
|||
<label htmlFor='amount'> |
|||
<CurrencyIcon currency={currency} crypto={crypto} /> |
|||
</label> |
|||
<input |
|||
type='text' |
|||
size='' |
|||
style={{ width: `${amount.length > 1 ? (amount.length * 15) - 5 : 25}%`, fontSize: `${190 - (amount.length ** 2)}px` }} |
|||
value={amount} |
|||
onChange={event => setAmount(event.target.value)} |
|||
id='amount' |
|||
/> |
|||
</section> |
|||
<section className={styles.inputContainer}> |
|||
<label htmlFor='paymentRequest'>Request:</label> |
|||
<input |
|||
type='text' |
|||
placeholder='Dinner, Rent, etc' |
|||
value={message} |
|||
onChange={event => setMessage(event.target.value)} |
|||
id='paymentRequest' |
|||
/> |
|||
</section> |
|||
<section className={styles.buttonGroup}> |
|||
<div className={styles.button} onClick={requestClicked}> |
|||
Request |
|||
</div> |
|||
</section> |
|||
</div> |
|||
) |
|||
} |
|||
|
|||
Request.propTypes = { |
|||
amount: PropTypes.string.isRequired, |
|||
setAmount: PropTypes.func.isRequired, |
|||
setMessage: PropTypes.func.isRequired, |
|||
createInvoice: PropTypes.func.isRequired, |
|||
message: PropTypes.string.isRequired, |
|||
currentTicker: PropTypes.object.isRequired, |
|||
currency: PropTypes.string.isRequired, |
|||
crypto: PropTypes.string.isRequired, |
|||
close: PropTypes.func.isRequired |
|||
} |
|||
|
|||
export default Request |
@ -1,3 +0,0 @@ |
|||
import Request from './Request' |
|||
|
|||
export default Request |
@ -0,0 +1,150 @@ |
|||
// Using bech32 here just without the 90 char length: https://github.com/bitcoinjs/bech32/blob/master/index.js
|
|||
|
|||
/* eslint-disable */ |
|||
|
|||
'use strict' |
|||
let ALPHABET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l' |
|||
|
|||
// pre-compute lookup table
|
|||
let ALPHABET_MAP = {} |
|||
for (let z = 0; z < ALPHABET.length; z++) { |
|||
let x = ALPHABET.charAt(z) |
|||
|
|||
if (ALPHABET_MAP[x] !== undefined) throw new TypeError(x + ' is ambiguous') |
|||
ALPHABET_MAP[x] = z |
|||
} |
|||
|
|||
function polymodStep (pre) { |
|||
let b = pre >> 25 |
|||
return ((pre & 0x1FFFFFF) << 5) ^ |
|||
(-((b >> 0) & 1) & 0x3b6a57b2) ^ |
|||
(-((b >> 1) & 1) & 0x26508e6d) ^ |
|||
(-((b >> 2) & 1) & 0x1ea119fa) ^ |
|||
(-((b >> 3) & 1) & 0x3d4233dd) ^ |
|||
(-((b >> 4) & 1) & 0x2a1462b3) |
|||
} |
|||
|
|||
function prefixChk (prefix) { |
|||
let chk = 1 |
|||
for (let i = 0; i < prefix.length; ++i) { |
|||
let c = prefix.charCodeAt(i) |
|||
if (c < 33 || c > 126) throw new Error('Invalid prefix (' + prefix + ')') |
|||
|
|||
chk = polymodStep(chk) ^ (c >> 5) |
|||
} |
|||
chk = polymodStep(chk) |
|||
|
|||
for (let i = 0; i < prefix.length; ++i) { |
|||
let v = prefix.charCodeAt(i) |
|||
chk = polymodStep(chk) ^ (v & 0x1f) |
|||
} |
|||
return chk |
|||
} |
|||
|
|||
function encode (prefix, words) { |
|||
// too long?
|
|||
if ((prefix.length + 7 + words.length) > 90) throw new TypeError('Exceeds Bech32 maximum length') |
|||
prefix = prefix.toLowerCase() |
|||
|
|||
// determine chk mod
|
|||
let chk = prefixChk(prefix) |
|||
let result = prefix + '1' |
|||
for (let i = 0; i < words.length; ++i) { |
|||
let x = words[i] |
|||
if ((x >> 5) !== 0) throw new Error('Non 5-bit word') |
|||
|
|||
chk = polymodStep(chk) ^ x |
|||
result += ALPHABET.charAt(x) |
|||
} |
|||
|
|||
for (let i = 0; i < 6; ++i) { |
|||
chk = polymodStep(chk) |
|||
} |
|||
chk ^= 1 |
|||
|
|||
for (let i = 0; i < 6; ++i) { |
|||
let v = (chk >> ((5 - i) * 5)) & 0x1f |
|||
result += ALPHABET.charAt(v) |
|||
} |
|||
|
|||
return result |
|||
} |
|||
|
|||
function decode (str) { |
|||
if (str.length < 8) throw new TypeError(str + ' too short') |
|||
// LN payment requests can be longer than 90 chars
|
|||
// if (str.length > 90) throw new TypeError(str + ' too long')
|
|||
|
|||
// don't allow mixed case
|
|||
let lowered = str.toLowerCase() |
|||
let uppered = str.toUpperCase() |
|||
if (str !== lowered && str !== uppered) throw new Error('Mixed-case string ' + str) |
|||
str = lowered |
|||
|
|||
let split = str.lastIndexOf('1') |
|||
if (split === 0) throw new Error('Missing prefix for ' + str) |
|||
|
|||
let prefix = str.slice(0, split) |
|||
let wordChars = str.slice(split + 1) |
|||
if (wordChars.length < 6) throw new Error('Data too short') |
|||
|
|||
let chk = prefixChk(prefix) |
|||
let words = [] |
|||
for (let i = 0; i < wordChars.length; ++i) { |
|||
let c = wordChars.charAt(i) |
|||
let v = ALPHABET_MAP[c] |
|||
if (v === undefined) throw new Error('Unknown character ' + c) |
|||
chk = polymodStep(chk) ^ v |
|||
|
|||
// not in the checksum?
|
|||
if (i + 6 >= wordChars.length) continue |
|||
words.push(v) |
|||
} |
|||
|
|||
if (chk !== 1) throw new Error('Invalid checksum for ' + str) |
|||
return { prefix, words } |
|||
} |
|||
|
|||
function convert (data, inBits, outBits, pad) { |
|||
let value = 0 |
|||
let bits = 0 |
|||
let maxV = (1 << outBits) - 1 |
|||
|
|||
let result = [] |
|||
for (let i = 0; i < data.length; ++i) { |
|||
value = (value << inBits) | data[i] |
|||
bits += inBits |
|||
|
|||
while (bits >= outBits) { |
|||
bits -= outBits |
|||
result.push((value >> bits) & maxV) |
|||
} |
|||
} |
|||
|
|||
if (pad) { |
|||
if (bits > 0) { |
|||
result.push((value << (outBits - bits)) & maxV) |
|||
} |
|||
} else { |
|||
if (bits >= inBits) throw new Error('Excess padding') |
|||
if ((value << (outBits - bits)) & maxV) throw new Error('Non-zero padding') |
|||
} |
|||
|
|||
return result |
|||
} |
|||
|
|||
function toWords (bytes) { |
|||
return convert(bytes, 8, 5, true) |
|||
} |
|||
|
|||
function fromWords (words) { |
|||
return convert(words, 5, 8, false) |
|||
} |
|||
|
|||
export default { |
|||
decode, |
|||
encode, |
|||
toWords, |
|||
fromWords |
|||
} |
|||
|
@ -1,7 +1,9 @@ |
|||
import btc from './btc' |
|||
import usd from './usd' |
|||
import bech32 from './bech32' |
|||
|
|||
export default { |
|||
btc, |
|||
usd |
|||
usd, |
|||
bech32 |
|||
} |
|||
|
@ -0,0 +1,60 @@ |
|||
import React from 'react' |
|||
import { shallow } from 'enzyme' |
|||
|
|||
import Form from '../../app/components/Form' |
|||
import PayForm from '../../app/components/Form/PayForm' |
|||
import RequestForm from '../../app/components/Form/RequestForm' |
|||
|
|||
const payFormProps = { |
|||
payform: {}, |
|||
currency: 'BTC', |
|||
crypto: 'BTC', |
|||
|
|||
isOnchain: false, |
|||
isLn: false, |
|||
currentAmount: '0', |
|||
inputCaption: '', |
|||
showPayLoadingScreen: false, |
|||
|
|||
setPayAmount: () => {}, |
|||
setPayInput: () => {}, |
|||
fetchInvoice: () => {}, |
|||
|
|||
|
|||
onPaySubmit: () => {} |
|||
} |
|||
|
|||
const requestFormProps = { |
|||
requestform: {}, |
|||
currency: '', |
|||
crypto: '', |
|||
|
|||
setRequestAmount: () => {}, |
|||
setRequestMemo: () => {}, |
|||
|
|||
onRequestSubmit: () => {} |
|||
} |
|||
|
|||
const defaultProps = { |
|||
formType: '', |
|||
formProps: {}, |
|||
closeForm: () => {} |
|||
} |
|||
|
|||
describe('Form', () => { |
|||
describe('should show pay form when formType is PAY_FORM', () => { |
|||
const props = { ...defaultProps, formType: 'PAY_FORM', formProps: payFormProps } |
|||
const el = shallow(<Form {...props} />) |
|||
it('should contain PayForm', () => { |
|||
expect(el.find(PayForm)).toHaveLength(1) |
|||
}) |
|||
}) |
|||
|
|||
describe('should show request form when formType is REQUEST_FORM', () => { |
|||
const props = { ...defaultProps, formType: 'REQUEST_FORM', formProps: requestFormProps } |
|||
const el = shallow(<Form {...props} />) |
|||
it('should contain RequestForm', () => { |
|||
expect(el.find(RequestForm)).toHaveLength(1) |
|||
}) |
|||
}) |
|||
}) |
@ -1,85 +1,13 @@ |
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP |
|||
|
|||
exports[`reducers formReducer should correctly resetForm 1`] = ` |
|||
exports[`reducers formReducer should correctly setFormType 1`] = ` |
|||
Object { |
|||
"amount": "0", |
|||
"formType": "pay", |
|||
"message": "", |
|||
"modalOpen": false, |
|||
"onchainAmount": "0", |
|||
"payment_request": "", |
|||
"pubkey": "", |
|||
} |
|||
`; |
|||
|
|||
exports[`reducers formReducer should correctly setAmount 1`] = ` |
|||
Object { |
|||
"amount": 1, |
|||
"formType": "pay", |
|||
"message": "", |
|||
"modalOpen": false, |
|||
"onchainAmount": "0", |
|||
"payment_request": "", |
|||
"pubkey": "", |
|||
} |
|||
`; |
|||
|
|||
exports[`reducers formReducer should correctly setForm 1`] = ` |
|||
Object { |
|||
"amount": "0", |
|||
"formType": "foo", |
|||
"message": "", |
|||
"modalOpen": true, |
|||
"onchainAmount": "0", |
|||
"payment_request": "", |
|||
"pubkey": "", |
|||
} |
|||
`; |
|||
|
|||
exports[`reducers formReducer should correctly setMessage 1`] = ` |
|||
Object { |
|||
"amount": "0", |
|||
"formType": "pay", |
|||
"message": "foo", |
|||
"modalOpen": false, |
|||
"onchainAmount": "0", |
|||
"payment_request": "", |
|||
"pubkey": "", |
|||
} |
|||
`; |
|||
|
|||
exports[`reducers formReducer should correctly setPaymentRequest 1`] = ` |
|||
Object { |
|||
"amount": "0", |
|||
"formType": "pay", |
|||
"message": "", |
|||
"modalOpen": false, |
|||
"onchainAmount": "0", |
|||
"payment_request": "foo", |
|||
"pubkey": "", |
|||
} |
|||
`; |
|||
|
|||
exports[`reducers formReducer should correctly setPubkey 1`] = ` |
|||
Object { |
|||
"amount": "0", |
|||
"formType": "pay", |
|||
"message": "", |
|||
"modalOpen": false, |
|||
"onchainAmount": "0", |
|||
"payment_request": "", |
|||
"pubkey": "foo", |
|||
"formType": "FOO", |
|||
} |
|||
`; |
|||
|
|||
exports[`reducers formReducer should handle initial state 1`] = ` |
|||
Object { |
|||
"amount": "0", |
|||
"formType": "pay", |
|||
"message": "", |
|||
"modalOpen": false, |
|||
"onchainAmount": "0", |
|||
"payment_request": "", |
|||
"pubkey": "", |
|||
"formType": null, |
|||
} |
|||
`; |
|||
|
@ -1,64 +1,26 @@ |
|||
import formReducer, { |
|||
SET_FORM, |
|||
SET_AMOUNT, |
|||
SET_MESSAGE, |
|||
SET_PUBKEY, |
|||
SET_PAYMENT_REQUEST, |
|||
RESET_FORM |
|||
SET_FORM_TYPE |
|||
} from '../../app/reducers/form' |
|||
|
|||
// describe('reducers', () => {
|
|||
// describe('formReducer', () => {
|
|||
|
|||
|
|||
// }
|
|||
// }
|
|||
|
|||
describe('reducers', () => { |
|||
describe('formReducer', () => { |
|||
it('should handle initial state', () => { |
|||
expect(formReducer(undefined, {})).toMatchSnapshot() |
|||
}) |
|||
|
|||
it('should have SET_FORM', () => { |
|||
expect(SET_FORM).toEqual('SET_FORM') |
|||
}) |
|||
|
|||
it('should have SET_AMOUNT', () => { |
|||
expect(SET_AMOUNT).toEqual('SET_AMOUNT') |
|||
}) |
|||
|
|||
it('should have SET_MESSAGE', () => { |
|||
expect(SET_MESSAGE).toEqual('SET_MESSAGE') |
|||
}) |
|||
|
|||
it('should have SET_PUBKEY', () => { |
|||
expect(SET_PUBKEY).toEqual('SET_PUBKEY') |
|||
}) |
|||
|
|||
it('should have SET_PAYMENT_REQUEST', () => { |
|||
expect(SET_PAYMENT_REQUEST).toEqual('SET_PAYMENT_REQUEST') |
|||
}) |
|||
|
|||
it('should have RESET_FORM', () => { |
|||
expect(RESET_FORM).toEqual('RESET_FORM') |
|||
}) |
|||
|
|||
it('should correctly setForm', () => { |
|||
expect(formReducer(undefined, { type: SET_FORM, modalOpen: true, formType: 'foo' })).toMatchSnapshot() |
|||
}) |
|||
|
|||
it('should correctly setAmount', () => { |
|||
expect(formReducer(undefined, { type: SET_AMOUNT, amount: 1 })).toMatchSnapshot() |
|||
}) |
|||
|
|||
it('should correctly setMessage', () => { |
|||
expect(formReducer(undefined, { type: SET_MESSAGE, message: 'foo' })).toMatchSnapshot() |
|||
}) |
|||
|
|||
it('should correctly setPubkey', () => { |
|||
expect(formReducer(undefined, { type: SET_PUBKEY, pubkey: 'foo' })).toMatchSnapshot() |
|||
}) |
|||
|
|||
it('should correctly setPaymentRequest', () => { |
|||
expect(formReducer(undefined, { type: SET_PAYMENT_REQUEST, payment_request: 'foo' })).toMatchSnapshot() |
|||
it('should have SET_FORM_TYPE', () => { |
|||
expect(SET_FORM_TYPE).toEqual('SET_FORM_TYPE') |
|||
}) |
|||
|
|||
it('should correctly resetForm', () => { |
|||
expect(formReducer(undefined, { type: RESET_FORM })).toMatchSnapshot() |
|||
it('should correctly setFormType', () => { |
|||
expect(formReducer(undefined, { type: SET_FORM_TYPE, formType: 'FOO' })).toMatchSnapshot() |
|||
}) |
|||
}) |
|||
}) |
|||
|
Loading…
Reference in new issue