diff --git a/src/app/components/PaymentRequest/index.tsx b/src/app/components/PaymentRequest/index.tsx new file mode 100644 index 00000000..9428918b --- /dev/null +++ b/src/app/components/PaymentRequest/index.tsx @@ -0,0 +1,140 @@ +import React from 'react'; +import { Alert } from 'antd'; +import './style.less'; + +import Identicon from 'components/Identicon'; +import Unit from 'components/Unit'; +import { unixMoment, SHORT_FORMAT } from 'utils/time'; +import moment from 'moment'; +import Loader from 'components/Loader'; +import { PaymentRequestData } from 'modules/payment/types'; + +import { PaymentRequestState } from 'modules/payment/types'; + +interface Props { + routedRequest: PaymentRequestData | null; + requestData: PaymentRequestState; +} + +interface State { + showMoreInfo: boolean; +} + +const INITIAL_STATE = { + showMoreInfo: false, +}; + +export default class PaymentRequest extends React.Component { + state: State = INITIAL_STATE; + + render() { + const { showMoreInfo } = this.state; + const { requestData, routedRequest } = this.props; + + const expiry = + requestData.data && + unixMoment(requestData.data.request.timestamp).add( + requestData.data.request.expiry, + 'seconds', + ); + + return ( +
+ {routedRequest ? ( +
+
+ +
+
+ {routedRequest.node.alias} +
+ + {routedRequest.node.pub_key} + +
+
+ {routedRequest.route ? ( +
+ + + {routedRequest.request.num_satoshis && ( + + + + + )} + + + + + + + + + {showMoreInfo && ( + <> + + + + + + + + + + + + + + )} + +
Amount + +
Fee + {requestData.isLoading ? ( + + ) : ( + + )} +
Total + {requestData.isLoading ? ( + + ) : ( + + )} +
Hops{routedRequest.route.hops.length} node(s)
Time lock + {moment() + .add(routedRequest.route.total_time_lock, 'seconds') + .fromNow(true)} +
Expires{expiry && expiry.format(SHORT_FORMAT)}
+ {!showMoreInfo && ( + this.setState({ showMoreInfo: true })} + > + More info + + )} +
+ ) : ( + + )} +
+ ) : ( + '' + )} +
+ ); + } +} diff --git a/src/app/components/PaymentRequest/style.less b/src/app/components/PaymentRequest/style.less new file mode 100644 index 00000000..399d03c0 --- /dev/null +++ b/src/app/components/PaymentRequest/style.less @@ -0,0 +1,133 @@ +@import '~style/variables.less'; + +.PaymentRequest { + &-pr { + margin-bottom: 1rem; + + &-input { + font-family: @code-family; + font-size: 0.65rem; + resize: none; + } + + &-qr { + position: absolute; + bottom: 0; + right: 10px; + } + } + + &-payment { + margin-bottom: 0.5rem; + + &-node { + display: flex; + padding: 0.6rem; + margin-bottom: 0.5rem; + + &-avatar { + flex-shrink: 0; + width: 2.8rem; + height: 2.8rem; + margin-right: 0.75rem; + } + + &-info { + flex: 1; + min-width: 0; + + &-alias { + font-size: 1.1rem; + font-weight: 500; + margin-bottom: 0.1rem; + } + + &-pubkey { + display: block; + font-size: 0.8rem; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } + } + } + + &-value { + padding: 0 1rem; + margin-bottom: 0.5rem; + + // Ant overrides + .ant-input-group.ant-input-group-compact { + display: flex; + } + + .ant-select-selection-selected-value { + font-size: 0.85rem; + } + } + + &-details { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + + table { + max-width: 280px; + width: 100%; + margin-bottom: 0.5rem; + + tr { + border-bottom: 1px solid rgba(#000, 0.05); + + &:last-child { + border: none; + } + + td { + padding: 0.2rem; + + &:first-child { + text-align: left; + min-width: 80px; + padding-right: 0.5rem; + } + + &:last-child { + text-align: right; + font-weight: 500; + } + } + } + } + + &-more { + padding: 0.5rem 0; + } + } + } + + &-placeholder { + display: flex; + justify-content: center; + align-items: center; + height: 8rem; + margin-bottom: 1rem; + color: rgba(#000, 0.2); + border: 2px dashed rgba(#000, 0.08); + border-radius: 4px; + } + + &-buttons { + display: flex; + + .ant-btn { + &:first-child { + margin-right: 0.5rem; + } + &:last-child { + flex: 1; + } + } + } +} diff --git a/src/app/prompts/lnurl.less b/src/app/prompts/lnurl.less index 6c40ce07..74dd3c1c 100644 --- a/src/app/prompts/lnurl.less +++ b/src/app/prompts/lnurl.less @@ -10,6 +10,29 @@ color: @text-color-secondary; } + &-metadata { + margin: 0; + padding: 0; + + &-item { + margin: 0; + padding: 0; + } + } + + &-buttons { + display: flex; + + .ant-btn { + &:first-child { + margin-right: 0.5rem; + } + &:last-child { + flex: 1; + } + } + } + &-header { display: flex; align-items: center; @@ -60,66 +83,5 @@ } } } - - &-memo { - &-text { - font-size: 1rem; - color: rgba(#000, 0.5); - - a { - margin-left: 0.5rem; - } - } - } - - &-advanced { - &-private { - display: flex; - align-items: center; - transform: translateY(-1rem); - } - - .ant-form-item { - flex: 1; - margin-right: 1rem; - - &:last-child { - margin-right: 0; - } - } - - .ant-form-item-label { - padding-bottom: 0; - - label { - display: flex; - align-items: center; - font-size: 0.7rem; - letter-spacing: 0.08rem; - - .Help { - margin-left: 0.25rem; - } - } - } - - .ant-form-explain { - font-size: 0.7rem; - } - } - - &-advancedToggle { - display: block; - text-align: center; - font-size: 0.8rem; - opacity: 0.7; - margin: 0 auto; - - &:hover, - &:focus, - &:active { - opacity: 1; - } - } } } diff --git a/src/app/prompts/lnurl.tsx b/src/app/prompts/lnurl.tsx index 355f6abf..915cfeb3 100644 --- a/src/app/prompts/lnurl.tsx +++ b/src/app/prompts/lnurl.tsx @@ -13,14 +13,22 @@ import { getNodeChain } from 'modules/node/selectors'; import { getChainRates } from 'modules/rates/selectors'; import { SendPaymentResponse } from 'webln'; import { PaymentRequestData } from 'modules/payment/types'; -import { sendPayment } from 'modules/payment/actions'; +import { + sendPayment, + resetSendPayment, + checkPaymentRequest, +} from 'modules/payment/actions'; import AmountField from 'components/AmountField'; +import PaymentRequest from 'components/PaymentRequest'; import { Denomination } from 'utils/constants'; import { fromBaseToUnit, fromUnitToBase } from 'utils/units'; -import { Form } from 'antd'; +import { Form, Button, Result } from 'antd'; import Loader from 'components/Loader'; import './lnurl.less'; import { getParams as getlnurlParams, LNURLPayParams } from 'js-lnurl'; +import { rejectPrompt, confirmPrompt } from 'utils/prompt'; + +import SendState from 'components/SendForm/SendState'; interface StateProps { paymentRequests: AppState['payment']['paymentRequests']; @@ -36,6 +44,8 @@ interface StateProps { interface DispatchProps { sendPayment: typeof sendPayment; + resetSendPayment: typeof resetSendPayment; + checkPaymentRequest: typeof checkPaymentRequest; } type Props = StateProps & DispatchProps; @@ -47,6 +57,7 @@ interface State { denomination: Denomination; routedRequest: PaymentRequestData | null; paymentRequestValue: string; + showMoreInfo: boolean; } const INITIAL_STATE = { @@ -56,6 +67,7 @@ const INITIAL_STATE = { denomination: Denomination.SATOSHIS, paymentRequestValue: '', routedRequest: null, + showMoreInfo: false, }; interface LnurlArgs { @@ -94,26 +106,76 @@ class LnurlPayPrompt extends React.Component { }); } + componentWillUnmount() { + this.props.resetSendPayment(); + } + + componentWillUpdate(nextProps: Props) { + const { paymentRequestValue } = this.state; + const oldPr = this.props.paymentRequests[paymentRequestValue]; + const newPr = nextProps.paymentRequests[paymentRequestValue]; + + if (newPr && newPr.data && newPr !== oldPr) { + this.setState({ routedRequest: newPr.data }); + } + } + render() { - const isConfirmDisabled = this.state.isLoading; + // Early exit for send state + const { sendLightningReceipt, isSending, sendError } = this.props; + if (isSending) { + return ; + } else if (sendLightningReceipt || sendError) { + const type = sendError ? 'error' : 'success'; + const closeButton = ( + + ); + return ( +
+ + {sendError || + (sendLightningReceipt && ( + <> +

Pre-image

+ {sendLightningReceipt.payment_preimage} + + ))} +
+
+ ); + } + + // not in sending state + + const { value, routedRequest, lnurlParams, paymentRequestValue } = this.state; + const requestData = this.props.paymentRequests[paymentRequestValue] || {}; + const isSubmitDisabled = + this.state.isLoading || + !notNilNum(this.state.value) || + (routedRequest !== null && routedRequest.route === null); const isValueDisabled = - this.state.lnurlParams && - this.state.lnurlParams.maxSendable === this.state.lnurlParams.minSendable; + lnurlParams && lnurlParams.maxSendable === lnurlParams.minSendable; return ( - +

- - Payment request from {removeDomainPrefix(this.origin.domain)} - + Pay request from {removeDomainPrefix(this.origin.domain)}

@@ -121,21 +183,40 @@ class LnurlPayPrompt extends React.Component { ) : (
- -
- - {this.renderMetadata()} - + + {this.renderMetadata()} + + {routedRequest ? ( + + ) : ( + + )} + +
+ +
)} @@ -145,6 +226,14 @@ class LnurlPayPrompt extends React.Component { ); } + private handleClose = () => { + return confirmPrompt(); + }; + + private handleReject = () => { + return rejectPrompt(); + }; + private renderMetadata = () => { const text: string = this.state.lnurlParams.decodedMetadata .filter(([typ, _]: any) => typ === 'text/plain') @@ -155,7 +244,7 @@ class LnurlPayPrompt extends React.Component { .map(([typ, content]: any) => `data:${typ},${content}`)[0]; return ( -
+
{image ? : null}

{text}

@@ -166,7 +255,33 @@ class LnurlPayPrompt extends React.Component { this.setState({ value }); }; - private handleConfirm = async (): Promise => { + private handleSubmit = async () => { + if (this.state.paymentRequestValue !== '') { + return this.handleSend(); + } else { + return this.getPaymentRequest(); + } + }; + + private handleSend = async () => { + const value = fromUnitToBase(this.state.value, this.state.denomination); + this.props.sendPayment({ + payment_request: this.state.paymentRequestValue, + amt: value, + }); + + const receipt = await watchUntilPropChange( + () => this.props.sendLightningReceipt, + () => this.props.sendError, + ); + + if (!receipt) { + throw new Error('Payment failed to send'); + } + return { preimage: receipt.payment_preimage }; + }; + + private getPaymentRequest = async () => { const { lnurlParams } = this.state; const value = fromUnitToBase(this.state.value, this.state.denomination); @@ -184,21 +299,18 @@ class LnurlPayPrompt extends React.Component { if (res.status === 'ERROR') { throw new Error(res.reason); } - - this.props.sendPayment({ - payment_request: res.pr, - amt: value, - }); - - const receipt = await watchUntilPropChange( - () => this.props.sendLightningReceipt, - () => this.props.sendError, + const paymentRequestValue = res.pr; + this.setState( + { + paymentRequestValue, + showMoreInfo: false, + }, + () => { + if (paymentRequestValue) { + this.props.checkPaymentRequest(paymentRequestValue); + } + }, ); - - if (!receipt) { - throw new Error('Payment failed to send'); - } - return { preimage: receipt.payment_preimage }; }; } @@ -214,5 +326,5 @@ export default connect( rates: getChainRates(state), chain: getNodeChain(state), }), - { sendPayment }, + { sendPayment, resetSendPayment, checkPaymentRequest }, )(LnurlPayPrompt);