From 04798ac7f99f47fe4b61c7c5c1cf7fc789d4a69d Mon Sep 17 00:00:00 2001 From: Jack Mallers <jimmymowschess@gmail.com> Date: Wed, 21 Feb 2018 11:39:07 -0600 Subject: [PATCH] feature(payform): new pay form MVP --- app/components/Form/Form.js | 21 +-- app/components/Form/Form.scss | 58 ++------ app/components/Form/Pay.js | 156 ++++++++++++++++++++++ app/components/Form/Pay.scss | 123 +++++++++++++++++ app/components/Wallet/Wallet.js | 5 +- app/icons/link.svg | 1 + app/icons/paper_plane.svg | 16 +++ app/icons/x.svg | 1 + app/icons/zap.svg | 1 + app/main.dev.js | 4 +- app/reducers/payform.js | 19 ++- app/routes/app/containers/AppContainer.js | 2 + app/utils/btc.js | 2 - 13 files changed, 345 insertions(+), 64 deletions(-) create mode 100644 app/components/Form/Pay.js create mode 100644 app/components/Form/Pay.scss create mode 100644 app/icons/link.svg create mode 100644 app/icons/paper_plane.svg create mode 100644 app/icons/x.svg create mode 100644 app/icons/zap.svg diff --git a/app/components/Form/Form.js b/app/components/Form/Form.js index dfc9476b..cbaece23 100644 --- a/app/components/Form/Form.js +++ b/app/components/Form/Form.js @@ -1,15 +1,19 @@ import React from 'react' import PropTypes from 'prop-types' +import Isvg from 'react-inlinesvg' import { MdClose } from 'react-icons/lib/md' +import Pay from './Pay' import PayForm from './PayForm' import RequestForm from './RequestForm' +import x from 'icons/x.svg' import styles from './Form.scss' const FORM_TYPES = { - PAY_FORM: PayForm, + // PAY_FORM: PayForm, + PAY_FORM: Pay, REQUEST_FORM: RequestForm } @@ -18,16 +22,13 @@ const Form = ({ formType, formProps, closeForm }) => { 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 className={`${styles.container} ${formType && styles.open}`}> + <div className={styles.closeContainer}> + <span onClick={closeForm}> + <Isvg src={x} /> + </span> </div> + <FormComponent {...formProps} /> </div> ) } diff --git a/app/components/Form/Form.scss b/app/components/Form/Form.scss index 25a54ff0..d0fa3879 100644 --- a/app/components/Form/Form.scss +++ b/app/components/Form/Form.scss @@ -1,59 +1,27 @@ @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: 20; - } -} - -.innercontainer { +.container { position: relative; height: 100vh; - margin: 5%; + background: $spaceblue; } -.esc { - position: absolute; - top: 0; - right: 0; - color: $darkestgrey; - cursor: pointer; - padding: 20px; - border-radius: 50%; +.closeContainer { + text-align: right; + padding: 20px 40px 0px; - &:hover { - color: $bluegrey; - background: $darkgrey; - } + span { + cursor: pointer; + opacity: 1.0; + transition: 0.25s all; - &:active { - color: $white; - background: $main; + &:hover { + opacity: 0.5; + } } svg { - width: 32px; - height: 32px; + color: $white; } } -.content { - width: 50%; - margin: 0 auto; - display: flex; - flex-direction: column; - height: 75vh; - justify-content: center; - align-items: center; -} diff --git a/app/components/Form/Pay.js b/app/components/Form/Pay.js new file mode 100644 index 00000000..a88dd950 --- /dev/null +++ b/app/components/Form/Pay.js @@ -0,0 +1,156 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' + +import Isvg from 'react-inlinesvg' +import paperPlane from 'icons/paper_plane.svg' + +import { FaBolt, FaChain, FaAngleDown } from 'react-icons/lib/fa' +import LoadingBolt from 'components/LoadingBolt' +import CurrencyIcon from 'components/CurrencyIcon' + +import styles from './Pay.scss' + +class Pay 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, showErrors }, + currency, + crypto, + + isOnchain, + isLn, + currentAmount, + usdAmount, + inputCaption, + showPayLoadingScreen, + payFormIsValid: { errors, isValid }, + + setPayAmount, + onPayAmountBlur, + + setPayInput, + onPayInputBlur, + + onPaySubmit + } = this.props + + console.log('usdAmount: ', usdAmount) + + return ( + <div className={styles.container}> + {showPayLoadingScreen && <LoadingBolt />} + <header className={styles.header}> + <Isvg src={paperPlane} /> + <h1>Make Payment</h1> + </header> + + <div className={styles.content}> + <section className={styles.destination}> + <div className={styles.top}> + <label htmlFor='destination'>Destination</label> + <span> + </span> + </div> + <div className={styles.bottom}> + <textarea + type='text' + placeholder='Payment request or bitcoin address' + value={payInput} + onChange={event => setPayInput(event.target.value)} + onBlur={onPayInputBlur} + id='destination' + rows='4' + /> + </div> + </section> + + <section className={styles.amount}> + <div className={styles.top}> + <label>Amount</label> + <span></span> + </div> + <div className={styles.bottom}> + <input + type='number' + min='0' + ref={(input) => { this.amountInput = input }} + size='' + placeholder='0.00000000' + value={currentAmount || ''} + onChange={event => setPayAmount(event.target.value)} + onBlur={onPayAmountBlur} + id='amount' + readOnly={isLn} + /> + <div className={styles.currency}> + <section className={styles.currentCurrency}> + <span>BTC</span><span><FaAngleDown /></span> + </section> + <ul> + <li>Bits</li> + <li>Satoshis</li> + </ul> + </div> + </div> + + <div className={styles.usdAmount}> + {`≈ ${usdAmount} USD`} + </div> + </section> + + <section className={styles.submit}> + <div className={`${styles.button} ${isValid && styles.active}`} onClick={onPaySubmit}>Pay</div> + </section> + </div> + </div> + ) + } +} + + +Pay.propTypes = { + payform: PropTypes.shape({ + amount: PropTypes.string.isRequired, + payInput: PropTypes.string.isRequired, + showErrors: PropTypes.object.isRequired + }).isRequired, + currency: PropTypes.string.isRequired, + crypto: PropTypes.string.isRequired, + + isOnchain: PropTypes.bool.isRequired, + isLn: PropTypes.bool.isRequired, + currentAmount: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number + ]), + inputCaption: PropTypes.string.isRequired, + showPayLoadingScreen: PropTypes.bool.isRequired, + 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 +} + +export default Pay diff --git a/app/components/Form/Pay.scss b/app/components/Form/Pay.scss new file mode 100644 index 00000000..98ec2b17 --- /dev/null +++ b/app/components/Form/Pay.scss @@ -0,0 +1,123 @@ +@import '../../variables.scss'; + +.container { + padding: 0 40px; + font-family: Roboto; +} + +.header { + text-align: center; + padding-bottom: 20px; + color: $white; + border-bottom: 1px solid $spaceborder; + + h1 { + font-size: 22px; + font-weight: 100; + margin-top: 10px; + letter-spacing: 1.5px; + } +} + +.content { + margin-top: 50px; + color: $white; + + .destination { + margin-bottom: 25px; + } + + .amount .bottom { + display: flex; + flex-direction: row; + align-items: center; + + input { + font-size: 40px; + max-width: 250px; + } + } + + .top { + margin-bottom: 30px; + + label { + font-size: 14px; + } + } + + .bottom { + input, textarea { + background: transparent; + outline: none; + border: 0; + color: $gold; + -webkit-text-fill-color: $white; + font-size: 12px; + width: 100%; + font-weight: 200; + } + + input::-webkit-input-placeholder, textarea::-webkit-input-placeholder { + text-shadow: none; + -webkit-text-fill-color: initial; + } + } + + .currency { + display: flex; + flex-direction: row; + align-items: center; + + .currentCurrency { + cursor: pointer; + transition: 0.25s all; + + &:hover { + opacity: 0.5; + } + + span { + font-size: 14px; + + &:nth-child(1) { + font-weight: bold; + } + } + + } + + ul { + visibility: hidden; + position: absolute; + } + } + + .usdAmount { + margin-top: 20px; + } + + .submit { + margin-top: 50px; + text-align: center; + + .button { + width: 235px; + margin: 0 auto; + padding: 20px 10px; + background: #31343f; + opacity: 0.5; + cursor: pointer; + transition: 0.25s all; + + &.active { + background: $gold; + opacity: 1.0; + + &:hover { + background: darken($gold, 5%); + } + } + } + } +} diff --git a/app/components/Wallet/Wallet.js b/app/components/Wallet/Wallet.js index 6bb2195a..703bc554 100644 --- a/app/components/Wallet/Wallet.js +++ b/app/components/Wallet/Wallet.js @@ -35,6 +35,7 @@ class Wallet extends Component { } = this.props const { modalOpen, qrCodeType } = this.state + const usdAmount = btc.satoshisToUsd((parseInt(balance.walletBalance, 10) + parseInt(balance.channelBalance, 10)), currentTicker.price_usd) const changeQrCode = () => { const qrCodeNum = this.state.qrCodeType === 1 ? 2 : 1 @@ -89,6 +90,7 @@ class Wallet extends Component { <Isvg className={styles.bitcoinLogo} src={qrCode} /> </span> </h1> + <span className={styles.usdValue}>≈ ${usdAmount ? usdAmount.toLocaleString() : ''}</span> <div className={styles.tickerButtons}> <section className={ticker.currency === 'btc' && styles.active} onClick={() => setCurrency('btc')}> BTC @@ -99,9 +101,6 @@ class Wallet extends Component { <section className={ticker.currency === 'sats' && styles.active} onClick={() => setCurrency('sats')}> Satoshis </section> - <section className={ticker.currency === 'usd' && styles.active} onClick={() => setCurrency('usd')}> - USD - </section> </div> </div> </div> diff --git a/app/icons/link.svg b/app/icons/link.svg new file mode 100644 index 00000000..c89dd41c --- /dev/null +++ b/app/icons/link.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg> \ No newline at end of file diff --git a/app/icons/paper_plane.svg b/app/icons/paper_plane.svg new file mode 100644 index 00000000..46ab5ce7 --- /dev/null +++ b/app/icons/paper_plane.svg @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> + <!-- Generator: Sketch 48.2 (47327) - http://www.bohemiancoding.com/sketch --> + <title>Shape</title> + <desc>Created with Sketch.</desc> + <defs></defs> + <g id="Pay-(Onchain)" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(-502.000000, -68.000000)" stroke-linecap="round" stroke-linejoin="round"> + <g id="Group-3" transform="translate(423.000000, 69.000000)" stroke="#FFFFFF" stroke-width="0.75"> + <g id="Group"> + <g id="send" transform="translate(79.000000, 0.000000)"> + <polygon id="Shape" points="21 0 13.65 21 9.45 11.55 0 7.35"></polygon> + </g> + </g> + </g> + </g> +</svg> \ No newline at end of file diff --git a/app/icons/x.svg b/app/icons/x.svg new file mode 100644 index 00000000..7d5875ca --- /dev/null +++ b/app/icons/x.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg> \ No newline at end of file diff --git a/app/icons/zap.svg b/app/icons/zap.svg new file mode 100644 index 00000000..8fdafa93 --- /dev/null +++ b/app/icons/zap.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-zap"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"></polygon></svg> \ No newline at end of file diff --git a/app/main.dev.js b/app/main.dev.js index e16f893a..e2f4f46f 100644 --- a/app/main.dev.js +++ b/app/main.dev.js @@ -152,8 +152,8 @@ const startLnd = (alias, autopilot) => { '--bitcoin.active', '--bitcoin.testnet', '--bitcoin.node=neutrino', - '--neutrino.connect=btcd.jackmallers.com:18333', - '--neutrino.addpeer=188.166.148.62:18333', + '--neutrino.connect=188.166.148.62:18333', + '--neutrino.addpeer=btcd.jackmallers.com:18333', '--neutrino.addpeer=159.65.48.139:18333', '--neutrino.connect=127.0.0.1:18333', '--debuglevel=debug', diff --git a/app/reducers/payform.js b/app/reducers/payform.js index f626af99..db17dbf2 100644 --- a/app/reducers/payform.js +++ b/app/reducers/payform.js @@ -135,17 +135,32 @@ payFormSelectors.isLn = createSelector( ) payFormSelectors.currentAmount = createSelector( + payFormSelectors.isLn, + payAmountSelector, + payInvoiceSelector, + (isLn, amount, invoice) => { + if (isLn) { + return btc.satoshisToBtc((invoice.num_satoshis || 0)) + } + + return amount > 0 ? amount : null + } +) + +payFormSelectors.usdAmount = createSelector( payFormSelectors.isLn, payAmountSelector, payInvoiceSelector, currencySelector, tickerSelectors.currentTicker, (isLn, amount, invoice, currency, ticker) => { + if (!ticker || !ticker.price_usd) { return false } + if (isLn) { - return currency === 'usd' ? btc.satoshisToUsd((invoice.num_satoshis || 0), ticker.price_usd) : btc.satoshisToBtc((invoice.num_satoshis || 0)) + return btc.satoshisToUsd((invoice.num_satoshis || 0), ticker.price_usd) } - return amount + return btc.btcToUsd(amount, ticker.price_usd) } ) diff --git a/app/routes/app/containers/AppContainer.js b/app/routes/app/containers/AppContainer.js index b1e4ab65..f0cc5faa 100644 --- a/app/routes/app/containers/AppContainer.js +++ b/app/routes/app/containers/AppContainer.js @@ -133,6 +133,7 @@ const mapStateToProps = state => ({ isOnchain: payFormSelectors.isOnchain(state), isLn: payFormSelectors.isLn(state), currentAmount: payFormSelectors.currentAmount(state), + usdAmount: payFormSelectors.usdAmount(state), inputCaption: payFormSelectors.inputCaption(state), showPayLoadingScreen: payFormSelectors.showPayLoadingScreen(state), payFormIsValid: payFormSelectors.payFormIsValid(state), @@ -159,6 +160,7 @@ const mergeProps = (stateProps, dispatchProps, ownProps) => { isOnchain: stateProps.isOnchain, isLn: stateProps.isLn, currentAmount: stateProps.currentAmount, + usdAmount: stateProps.usdAmount, inputCaption: stateProps.inputCaption, showPayLoadingScreen: stateProps.showPayLoadingScreen, payFormIsValid: stateProps.payFormIsValid, diff --git a/app/utils/btc.js b/app/utils/btc.js index 93e9edc9..32c470d1 100644 --- a/app/utils/btc.js +++ b/app/utils/btc.js @@ -39,8 +39,6 @@ export function renderCurrency(currency) { return 'bits' case 'sats': return 'satoshis' - case 'usd': - return 'USD' default: return 'satoshis' }