diff --git a/.eslintrc b/.eslintrc index 87ba9521..69ccec87 100644 --- a/.eslintrc +++ b/.eslintrc @@ -23,6 +23,7 @@ "indent": 2, "jsx-quotes": ["error", "prefer-single"], "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], + "react/no-did-mount-set-state": 0, "jsx-a11y/no-static-element-interactions": 0, "jsx-a11y/no-noninteractive-element-interactions": 0, "jsx-a11y/click-events-have-key-events": 0, @@ -41,12 +42,14 @@ "import/no-extraneous-dependencies": 0, "no-new": 0, "compat/compat": "error", - "flowtype-errors/show-errors": "error", - "flowtype-errors/enforce-min-coverage": ["error", 20] + "prefer-destructuring": ["error", { + "array": false, + "object": true + }], + "prefer-promise-reject-errors": 0 }, "plugins": [ "flowtype", - "flowtype-errors", "import", "promise", "compat", diff --git a/app/.eslintrc b/app/.eslintrc index 6d3c12ad..90d8894e 100644 --- a/app/.eslintrc +++ b/app/.eslintrc @@ -1,22 +1,3 @@ { - "rules": { - "flowtype/boolean-style": [2, "boolean"], - "flowtype/define-flow-type": 1, - "flowtype/delimiter-dangle": [2, "never"], - "flowtype/generic-spacing": [2, "never"], - "flowtype/no-primitive-constructor-types": 2, - "flowtype/no-weak-types": 1, - "flowtype/object-type-delimiter": [2, "comma"], - "flowtype/require-parameter-type": 0, - "flowtype/require-return-type": 0, - "flowtype/require-valid-file-annotation": 0, - "flowtype/semi": [2, "always"], - "flowtype/space-after-type-colon": [2, "always"], - "flowtype/space-before-generic-bracket": [2, "never"], - "flowtype/space-before-type-colon": [2, "never"], - "flowtype/union-intersection-spacing": [2, "always"], - "flowtype/use-flow-type": 2, - "flowtype/valid-syntax": 2, - "flowtype-errors/show-errors": 2 - } + "rules": {} } diff --git a/app/animated_checkmark.scss b/app/animated_checkmark.scss index 0eb592f2..48c9e0db 100644 --- a/app/animated_checkmark.scss +++ b/app/animated_checkmark.scss @@ -5,19 +5,19 @@ $curve: cubic-bezier(0.650, 0.000, 0.450, 1.000); stroke-dashoffset: 166; stroke-width: 2; stroke-miterlimit: 10; - stroke: $main; + stroke: $green; fill: none; animation: stroke .6s $curve forwards; } .checkmark { - width: 56px; - height: 56px; + width: 20px; + height: 20px; border-radius: 50%; stroke-width: 2; stroke: #fff; stroke-miterlimit: 10; - box-shadow: inset 0px 0px 0px $main; + box-shadow: inset 0px 0px 0px $green; animation: fill .4s ease-in-out .4s forwards, scale .3s ease-in-out .9s both; } @@ -45,6 +45,6 @@ $curve: cubic-bezier(0.650, 0.000, 0.450, 1.000); @keyframes fill { 100% { - box-shadow: inset 0px 0px 0px 30px $main; + box-shadow: inset 0px 0px 0px 30px $green; } } \ No newline at end of file diff --git a/app/components/Activity/ActivityModal.js b/app/components/Activity/ActivityModal.js new file mode 100644 index 00000000..b255ece3 --- /dev/null +++ b/app/components/Activity/ActivityModal.js @@ -0,0 +1,57 @@ +import React from 'react' +import PropTypes from 'prop-types' +import Isvg from 'react-inlinesvg' +import x from 'icons/x.svg' + +import TransactionModal from './TransactionModal' +import PaymentModal from './PaymentModal' +import InvoiceModal from './InvoiceModal' + +import styles from './ActivityModal.scss' + +const ActivityModal = ({ + modalType, + modalProps, + ticker, + currentTicker, + + hideActivityModal, + toggleCurrencyProps +}) => { + const MODAL_COMPONENTS = { + TRANSACTION: TransactionModal, + PAYMENT: PaymentModal, + INVOICE: InvoiceModal + } + + if (!modalType) { return null } + + const SpecificModal = MODAL_COMPONENTS[modalType] + return ( +
+
+ hideActivityModal()}> + + +
+ +
+ ) +} + +ActivityModal.propTypes = { + ticker: PropTypes.object.isRequired, + currentTicker: PropTypes.object.isRequired, + toggleCurrencyProps: PropTypes.object.isRequired, + + modalType: PropTypes.string, + modalProps: PropTypes.object.isRequired, + hideActivityModal: PropTypes.func.isRequired +} + +export default ActivityModal diff --git a/app/components/Activity/ActivityModal.scss b/app/components/Activity/ActivityModal.scss new file mode 100644 index 00000000..eeafdf6c --- /dev/null +++ b/app/components/Activity/ActivityModal.scss @@ -0,0 +1,27 @@ +@import '../../variables.scss'; + +.container { + position: relative; + height: 100vh; + background: $bluegrey; +} + +.closeContainer { + text-align: right; + padding: 20px 40px 0px; + + span { + cursor: pointer; + opacity: 1.0; + transition: 0.25s all; + + &:hover { + opacity: 0.5; + } + } + + svg { + color: $white; + } +} + diff --git a/app/components/Activity/Countdown.js b/app/components/Activity/Countdown.js new file mode 100644 index 00000000..8ac26986 --- /dev/null +++ b/app/components/Activity/Countdown.js @@ -0,0 +1,94 @@ +import React from 'react' +import PropTypes from 'prop-types' + +import styles from './Countdown.scss' + +class Countdown extends React.Component { + constructor(props) { + super(props) + + this.state = { + days: null, + hours: null, + minutes: null, + seconds: null, + expired: false, + interval: null + } + + this.timerInterval = this.timerInterval.bind(this) + } + + componentDidMount() { + const interval = setInterval(this.timerInterval, 1000) + // store interval in the state so it can be accessed later + this.setState({ interval }) + } + + componentWillUnmount() { + // use interval from the state to clear the interval + clearInterval(this.state.interval) + } + + timerInterval() { + const convertTwoDigits = n => (n > 9 ? n : `0${n}`.slice(-2)) + + const now = new Date().getTime() + const distance = (this.props.countDownDate * 1000) - now + + if (distance <= 0) { + this.setState({ expired: true }) + clearInterval(this.state.interval) + return + } + + const days = convertTwoDigits(Math.floor(distance / (1000 * 60 * 60 * 24))) + const hours = convertTwoDigits(Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))) + const minutes = convertTwoDigits(Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60))) + const seconds = convertTwoDigits(Math.floor((distance % (1000 * 60)) / 1000)) + + this.setState({ + days, + hours, + minutes, + seconds + }) + } + + render() { + const { + days, + hours, + minutes, + seconds, + expired + } = this.state + + if (expired) { return Expired } + if (!days && !hours && !minutes && !seconds) { return } + + return ( + + Expires in + + {days > 0 && `${days}:`} + + + {hours > 0 && `${hours}:`} + + + {minutes > 0 && `${minutes}:`} + + + {seconds >= 0 && `${seconds}`} + + + ) + } +} + +Countdown.propTypes = { + countDownDate: PropTypes.number.isRequired +} + +export default Countdown diff --git a/app/components/Activity/Countdown.scss b/app/components/Activity/Countdown.scss new file mode 100644 index 00000000..9f71c674 --- /dev/null +++ b/app/components/Activity/Countdown.scss @@ -0,0 +1,16 @@ +@import '../../variables.scss'; + +.container { + display: block; + text-align: center; + font-size: 12px; + min-height: 12px; + + &.expired { + color: $red; + } +} + +.caption { + margin-right: 4px; +} \ No newline at end of file diff --git a/app/components/Activity/InvoiceModal.js b/app/components/Activity/InvoiceModal.js new file mode 100644 index 00000000..e796d719 --- /dev/null +++ b/app/components/Activity/InvoiceModal.js @@ -0,0 +1,107 @@ +import React from 'react' +import PropTypes from 'prop-types' + +import Moment from 'react-moment' +import 'moment-timezone' + +import QRCode from 'qrcode.react' +import copy from 'copy-to-clipboard' +import { showNotification } from 'notifications' + +import { FaAngleDown } from 'react-icons/lib/fa' + +import Value from 'components/Value' +import Countdown from './Countdown' + +import styles from './InvoiceModal.scss' + +const InvoiceModal = ({ + invoice, + ticker, + currentTicker, + + toggleCurrencyProps: { + setActivityModalCurrencyFilters, + showCurrencyFilters, + currencyName, + currentCurrencyFilters, + onCurrencyFilterClick + } +}) => { + const copyPaymentRequest = () => { + copy(invoice.payment_request) + showNotification('Noice', 'Successfully copied to clipboard') + } + + const countDownDate = (parseInt(invoice.creation_date, 10) + parseInt(invoice.expiry, 10)) + + return ( +
+
+
+

Payment Request

+ + +
+
+
+
+

+ +

+
setActivityModalCurrencyFilters(!showCurrencyFilters)}> + {currencyName} +
+
    + { + currentCurrencyFilters.map(filter => +
  • onCurrencyFilterClick(filter.key)}>{filter.name}
  • ) + } +
+
+
+

+ {invoice.creation_date * 1000} +

+

+ {!invoice.settled && 'Not Paid'} +

+
+
+ +
+

Memo

+

{invoice.memo}

+
+ +
+

Request

+

{invoice.payment_request}

+
+
+
+ +
+
Save as image
+
Copy Request
+
+
+ ) +} + +InvoiceModal.propTypes = { + invoice: PropTypes.object.isRequired, + ticker: PropTypes.object.isRequired, + currentTicker: PropTypes.object.isRequired, + toggleCurrencyProps: PropTypes.object.isRequired +} + +export default InvoiceModal diff --git a/app/components/Activity/InvoiceModal.scss b/app/components/Activity/InvoiceModal.scss new file mode 100644 index 00000000..b805b647 --- /dev/null +++ b/app/components/Activity/InvoiceModal.scss @@ -0,0 +1,152 @@ +@import '../../variables.scss'; + +.container { + color: $white; +} + +.content { + display: flex; + flex-direction: row; + align-items: center; + background: $spaceblue; + width: 85%; + margin: 50px auto; + padding: 30px 0; + + .left { + width: 25%; + padding: 0 60px; + + h2 { + text-align: center; + margin-bottom: 20px; + } + + .qrcode { + margin-bottom: 20px; + } + } + + .right { + width: 75%; + min-height: 220px; + border-left: 1px solid $spaceborder; + padding: 10px 60px; + + .details { + display: flex; + flex-direction: row; + justify-content: space-between; + margin-bottom: 40px; + + .amount { + display: flex; + flex-direction: row; + align-items: center; + position: relative; + + h1 { + font-size: 40px; + margin-right: 10px; + } + + .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; + top: 40px; + right: -50px; + + &.active { + visibility: visible; + } + + li { + padding: 8px 15px; + background: #191919; + cursor: pointer; + transition: 0.25s hover; + border-bottom: 1px solid #0f0f0f; + + &:hover { + background: #0f0f0f; + } + } + } + } + + .date { + font-size: 12px; + text-align: right; + + .notPaid { + color: #FF8A65; + margin-top: 5px; + } + } + } + + .memo, .request { + h4 { + font-size: 10px; + margin-bottom: 10px; + } + + p { + word-wrap: break-word; + max-width: 450px; + } + } + + .memo { + margin-bottom: 40px; + + p { + font-size: 20px; + } + } + + .request p { + font-size: 10px; + max-width: 450px; + line-height: 1.5; + } + } +} + +.actions { + display: flex; + flex-direction: row; + justify-content: center; + + div { + text-align: center; + margin: 35px 10px; + width: 235px; + padding: 20px 10px; + background: #31343f; + cursor: pointer; + transition: 0.25s all; + + &:hover { + background: darken(#31343f, 5%); + } + } +} diff --git a/app/components/Activity/PaymentModal.js b/app/components/Activity/PaymentModal.js new file mode 100644 index 00000000..01cfed78 --- /dev/null +++ b/app/components/Activity/PaymentModal.js @@ -0,0 +1,83 @@ +import React from 'react' +import PropTypes from 'prop-types' + +import Moment from 'react-moment' +import 'moment-timezone' + +import { FaAngleDown } from 'react-icons/lib/fa' + +import Isvg from 'react-inlinesvg' +import paperPlane from 'icons/paper_plane.svg' +import zap from 'icons/zap.svg' + +import Value from 'components/Value' + +import styles from './PaymentModal.scss' + +const PaymentModal = ({ + payment, + ticker, + currentTicker, + + toggleCurrencyProps: { + setActivityModalCurrencyFilters, + showCurrencyFilters, + currencyName, + currentCurrencyFilters, + onCurrencyFilterClick + } +}) => ( +
+
+
+ + Sent +
+
+
+ + Lightning Network +
+
+ + {currencyName} fee +
+
+
+ +
+

+ 0 && styles.active}`}>- + +

+
setActivityModalCurrencyFilters(!showCurrencyFilters)}> + {currencyName} +
    + { + currentCurrencyFilters.map(filter => +
  • onCurrencyFilterClick(filter.key)}>{filter.name}
  • ) + } +
+
+ +
+ +
+ {payment.creation_date * 1000} +
+ +
+

{payment.payment_preimage}

+
+
+) + +PaymentModal.propTypes = { + payment: PropTypes.object.isRequired, + ticker: PropTypes.object.isRequired, + currentTicker: PropTypes.object.isRequired, + + toggleCurrencyProps: PropTypes.object.isRequired +} + +export default PaymentModal diff --git a/app/components/Activity/PaymentModal.scss b/app/components/Activity/PaymentModal.scss new file mode 100644 index 00000000..d7a648af --- /dev/null +++ b/app/components/Activity/PaymentModal.scss @@ -0,0 +1,126 @@ +@import '../../variables.scss'; + +@import '../../variables.scss'; + +.container { + color: $white; + font-size: 12px; + width: 75%; + margin: 0 auto; + background: $spaceblue; +} + +.header { + display: flex; + flex-direction: row; + justify-content: space-between; + padding: 20px; + + section { + &:nth-child(1) { + font-size: 16px; + color: $green; + + svg { + width: 16px; + height: 16px; + vertical-align: top; + fill: $green; + } + + span:nth-child(2) { + margin-left: 5px; + } + } + + &.details { + text-align: right; + + div:nth-child(1) { + margin-bottom: 5px; + } + + svg { + width: 12px; + height: 12px; + vertical-align: middle; + } + + .zap { + margin-left: 5px; + cursor: pointer; + transition: all 0.25s; + } + } + } +} + +.amount { + margin-top: 50px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + padding: 20px; + + h1 { + font-size: 40px; + } + + section { + font-size: 20px; + margin-left: 10px; + position: relative; + cursor: pointer; + + &:hover { + span { + opacity: 0.5; + } + } + + span { + transition: all 0.25s; + } + + ul { + visibility: hidden; + position: absolute; + top: 40px; + right: -50px; + font-size: 12px; + + &.active { + visibility: visible; + } + + li { + padding: 8px 15px; + background: #191919; + cursor: pointer; + transition: 0.25s hover; + border-bottom: 1px solid #0f0f0f; + + &:hover { + background: #0f0f0f; + } + } + } + } +} + +.date { + text-align: center; + padding: 20px; +} + +.footer { + background: #31343f; + margin: 20px 0 50px 0; + padding: 20px; + text-align: center; + + p { + cursor: pointer; + } +} \ No newline at end of file diff --git a/app/components/Activity/TransactionModal.js b/app/components/Activity/TransactionModal.js new file mode 100644 index 00000000..072857fe --- /dev/null +++ b/app/components/Activity/TransactionModal.js @@ -0,0 +1,91 @@ +import React from 'react' +import PropTypes from 'prop-types' + +import Moment from 'react-moment' +import 'moment-timezone' + +import { FaAngleDown } from 'react-icons/lib/fa' + +import Isvg from 'react-inlinesvg' +import paperPlane from 'icons/paper_plane.svg' +import link from 'icons/link.svg' +import { blockExplorer } from 'utils' + +import Value from 'components/Value' + +import styles from './TransactionModal.scss' + +const TransactionModal = ({ + transaction, + ticker, + currentTicker, + + toggleCurrencyProps: { + setActivityModalCurrencyFilters, + showCurrencyFilters, + currencyName, + currentCurrencyFilters, + onCurrencyFilterClick + } +}) => ( +
+
+
+ + Sent +
+
+
+ + blockExplorer.showTransaction(transaction.tx_hash)}>On-Chain +
+
+ + {currencyName} fee +
+
+
+ +
+

+ 0 && styles.active}`}> + { + transaction.amount > 0 ? + '+' + : + '-' + } + + +

+
setActivityModalCurrencyFilters(!showCurrencyFilters)}> + {currencyName} +
    + { + currentCurrencyFilters.map(filter => +
  • onCurrencyFilterClick(filter.key)}>{filter.name}
  • ) + } +
+
+ +
+ +
+ {transaction.time_stamp * 1000} +
+ +
+

blockExplorer.showTransaction(transaction.tx_hash)}>{transaction.tx_hash}

+
+
+) + +TransactionModal.propTypes = { + transaction: PropTypes.object.isRequired, + ticker: PropTypes.object.isRequired, + currentTicker: PropTypes.object.isRequired, + + toggleCurrencyProps: PropTypes.object.isRequired +} + +export default TransactionModal diff --git a/app/components/Activity/TransactionModal.scss b/app/components/Activity/TransactionModal.scss new file mode 100644 index 00000000..15c875c8 --- /dev/null +++ b/app/components/Activity/TransactionModal.scss @@ -0,0 +1,135 @@ +@import '../../variables.scss'; + +.container { + color: $white; + font-size: 12px; + width: 75%; + margin: 0 auto; + background: $spaceblue; +} + +.header { + display: flex; + flex-direction: row; + justify-content: space-between; + padding: 20px; + + section { + &:nth-child(1) { + font-size: 16px; + color: $green; + + svg { + width: 16px; + height: 16px; + vertical-align: top; + fill: $green; + } + + span:nth-child(2) { + margin-left: 5px; + } + } + + &.details { + text-align: right; + + div:nth-child(1) { + margin-bottom: 5px; + } + + svg { + width: 12px; + height: 12px; + vertical-align: middle; + } + + .link { + text-decoration: underline; + margin-left: 5px; + cursor: pointer; + transition: all 0.25s; + + &:hover { + opacity: 0.5; + } + } + } + } +} + +.amount { + margin-top: 50px; + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + padding: 20px; + + h1 { + font-size: 40px; + } + + section { + font-size: 20px; + margin-left: 10px; + position: relative; + cursor: pointer; + + &:hover { + span { + opacity: 0.5; + } + } + + span { + transition: all 0.25s; + } + + ul { + visibility: hidden; + position: absolute; + top: 40px; + right: -50px; + font-size: 12px; + + &.active { + visibility: visible; + } + + li { + padding: 8px 15px; + background: #191919; + cursor: pointer; + transition: 0.25s hover; + border-bottom: 1px solid #0f0f0f; + + &:hover { + background: #0f0f0f; + } + } + } + } +} + +.date { + text-align: center; + margin: 20px 0 50px 0; + padding: 20px; +} + +.footer { + background: #31343f; + padding: 20px; + text-align: center; + + p { + text-decoration: underline; + cursor: pointer; + transition: all 0.25s; + + &:hover { + opacity: 0.5; + } + } +} \ No newline at end of file diff --git a/app/components/Form/Form.js b/app/components/Form/Form.js index dfc9476b..9346fbd5 100644 --- a/app/components/Form/Form.js +++ b/app/components/Form/Form.js @@ -1,16 +1,17 @@ import React from 'react' import PropTypes from 'prop-types' -import { MdClose } from 'react-icons/lib/md' +import Isvg from 'react-inlinesvg' +import x from 'icons/x.svg' -import PayForm from './PayForm' -import RequestForm from './RequestForm' +import Pay from './Pay' +import Request from './Request' import styles from './Form.scss' const FORM_TYPES = { - PAY_FORM: PayForm, - REQUEST_FORM: RequestForm + PAY_FORM: Pay, + REQUEST_FORM: Request } const Form = ({ formType, formProps, closeForm }) => { @@ -18,16 +19,13 @@ const Form = ({ formType, formProps, closeForm }) => { const FormComponent = FORM_TYPES[formType] return ( -
-
-
- -
- -
- -
+
+
+ + +
+
) } diff --git a/app/components/Form/Form.scss b/app/components/Form/Form.scss index 25a54ff0..703c1713 100644 --- a/app/components/Form/Form.scss +++ b/app/components/Form/Form.scss @@ -1,59 +1,26 @@ @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..58c0546b --- /dev/null +++ b/app/components/Form/Pay.js @@ -0,0 +1,219 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import find from 'lodash/find' + +import Isvg from 'react-inlinesvg' +import paperPlane from 'icons/paper_plane.svg' +import link from 'icons/link.svg' +import { FaAngleDown } from 'react-icons/lib/fa' + +import { btc } from 'utils' + +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: { + payInput, + showErrors, + invoice, + showCurrencyFilters + }, + nodes, + ticker, + + isOnchain, + isLn, + currentAmount, + usdAmount, + payFormIsValid: { errors, isValid }, + currentCurrencyFilters, + currencyName, + + setPayAmount, + onPayAmountBlur, + + setPayInput, + onPayInputBlur, + + setCurrencyFilters, + + onPaySubmit, + + setCurrency + } = this.props + + const displayNodeName = (pubkey) => { + const node = find(nodes, 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) + setCurrencyFilters(false) + } + + return ( +
+
+ +

Make Payment

+
+ +
+
+
+ + + {isOnchain && + + + On-Chain (~10 minutes) + + } + {isLn && + + + {displayNodeName(invoice.destination)} ({invoice.description}) + + + } + +
+
+