Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 126 additions & 36 deletions frontends/web/src/routes/lightning/receive/receive.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// SPDX-License-Identifier: Apache-2.0

import { ChangeEvent, useCallback, useEffect, useRef, useState } from 'react';
import { ChangeEvent, useCallback, useContext, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Column, Grid, GuideWrapper, GuidedContent, Header, Main } from '../../../components/layout';
import { View, ViewButtons, ViewContent } from '../../../components/view/view';
import { Button, Input, OptionalLabel } from '../../../components/forms';
import { Button, Input, NumberInput, OptionalLabel } from '../../../components/forms';
import {
TLightningPayment,
TReceivePaymentResponse,
Expand All @@ -13,36 +13,52 @@ import {
getReceivePayment,
subscribeListPayments
} from '../../../api/lightning';
import { convertFromCurrency, convertToCurrency, getBtcSatsAmount } from '@/api/coins';
import { Status } from '../../../components/status/status';
import { QRCode } from '../../../components/qrcode/qrcode';
import { unsubscribe } from '../../../utils/subscriptions';
import { Spinner } from '../../../components/spinner/Spinner';
import { Checked, Copy, EditActive } from '../../../components/icon';
import { getBtcSatsAmount } from '../../../api/coins';
import { TAmountWithConversions } from '../../../api/account';
import { AmountWithUnit } from '@/components/amount/amount-with-unit';
import { useNavigate } from 'react-router-dom';
import { RatesContext } from '@/contexts/RatesContext';
import { useMountedRef } from '@/hooks/mount';
import styles from './receive.module.css';

type TStep = 'create-invoice' | 'wait' | 'invoice' | 'success';

const parseSatsAmount = (amount: string): number | undefined => {
if (!/^\d+$/.test(amount)) {
return undefined;
}
const parsedAmount = Number(amount);
return Number.isFinite(parsedAmount) && Number.isInteger(parsedAmount) && parsedAmount > 0
? parsedAmount
: undefined;
};

export function Receive() {
const { t } = useTranslation();
const navigate = useNavigate();
const { defaultCurrency } = useContext(RatesContext);
const mounted = useMountedRef();
const amountRequestId = useRef(0);
const [inputSatsText, setInputSatsText] = useState<string>('');
const [inputFiatText, setInputFiatText] = useState<string>('');
const [invoiceAmount, setInvoiceAmount] = useState<TAmountWithConversions>();
const [description, setDescription] = useState<string>('');
const [disableConfirm, setDisableConfirm] = useState(true);
const [receivePaymentResponse, setReceivePaymentResponse] = useState<TReceivePaymentResponse>();
const [receiveError, setReceiveError] = useState<string>();
const [step, setStep] = useState<TStep>('create-invoice');
const [payments, setPayments] = useState<TLightningPayment[]>();
const satsAmount = parseSatsAmount(inputSatsText);

const newInvoice = useCallback(() => {
setInputSatsText('');
setInputFiatText('');
setInvoiceAmount(undefined);
setDescription('');
setDisableConfirm(true);
setReceivePaymentResponse(undefined);
setReceiveError(undefined);
setStep('create-invoice');
Expand All @@ -60,15 +76,96 @@ export function Receive() {
setReceiveError(undefined);
if (step === 'success') {
setInputSatsText('');
setInputFiatText('');
}
break;
}
}, [step, navigate]);

const onAmountSatsChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
const target = event.target as HTMLInputElement;
setInputSatsText(target.value);
}, []);
const updateAmountFromSats = useCallback(async (satsText: string, updateFiat: boolean) => {
const requestId = ++amountRequestId.current;
setInvoiceAmount(undefined);
if (updateFiat) {
setInputFiatText('');
}

if (!satsText) {
return;
}

const response = await getBtcSatsAmount(satsText);
if (!mounted.current || requestId !== amountRequestId.current) {
return;
}
if (response.success) {
setInvoiceAmount(response.amount);
if (updateFiat) {
const fiatResponse = await convertToCurrency({
amount: response.amount.amount,
coinCode: 'btc',
fiatUnit: defaultCurrency,
});
if (!mounted.current || requestId !== amountRequestId.current) {
return;
}
setInputFiatText(fiatResponse.success ? fiatResponse.fiatAmount : '');
}
}
}, [defaultCurrency, mounted]);

const updateAmountFromFiat = useCallback(async (fiatText: string) => {
const requestId = ++amountRequestId.current;
setInputSatsText('');
setInvoiceAmount(undefined);

if (!fiatText) {
return;
}

const coinAmountResponse = await convertFromCurrency({
amount: fiatText,
coinCode: 'btc',
fiatUnit: defaultCurrency,
});
if (!mounted.current || requestId !== amountRequestId.current) {
return;
}
if (!coinAmountResponse.success) {
return;
}

const satsAmountResponse = await convertToCurrency({
amount: coinAmountResponse.amount,
coinCode: 'btc',
fiatUnit: 'sat',
});
if (!mounted.current || requestId !== amountRequestId.current) {
return;
}
if (!satsAmountResponse.success) {
return;
}

const satsText = satsAmountResponse.fiatAmount;
setInputSatsText(satsText);
const satsResponse = await getBtcSatsAmount(satsText);
if (!mounted.current || requestId !== amountRequestId.current) {
return;
}
if (satsResponse.success) {
setInvoiceAmount(satsResponse.amount);
}
}, [defaultCurrency, mounted]);

const onAmountSatsChange = useCallback((amount: string) => {
setInputSatsText(amount);
updateAmountFromSats(amount, true);
}, [updateAmountFromSats]);

const onAmountFiatChange = useCallback((amount: string) => {
setInputFiatText(amount);
updateAmountFromFiat(amount);
}, [updateAmountFromFiat]);

const onDescriptionChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
const target = event.target as HTMLInputElement;
Expand All @@ -84,26 +181,6 @@ export function Receive() {
return () => unsubscribe(subscriptions);
}, [onPaymentsChange]);

useEffect(() => {
getBtcSatsAmount(inputSatsText).then((response) => {
if (response.success) {
setInvoiceAmount(response.amount);
}
});
}, [inputSatsText]);


useEffect(() => {
(async () => {
const inputSats = Number(inputSatsText);
if (inputSats > 0) {
setDisableConfirm(false);
return;
}
setDisableConfirm(true);
})();
}, [inputSatsText]);

useEffect(() => {
if (payments && receivePaymentResponse && step === 'invoice') {
const payment = payments.find((payment) => payment.type === 'receive' && payment.invoice === receivePaymentResponse.invoice);
Expand All @@ -115,10 +192,14 @@ export function Receive() {

const receivePayment = useCallback(async () => {
setReceiveError(undefined);
if (satsAmount === undefined) {
setReceiveError(t('send.error.invalidAmount'));
return;
}
setStep('wait');
try {
const receivePaymentResponse = await getReceivePayment({
amountSat: Number(inputSatsText),
amountSat: satsAmount,
description,
});
setReceivePaymentResponse(receivePaymentResponse);
Expand All @@ -131,7 +212,7 @@ export function Receive() {
setReceiveError(String(e));
}
}
}, [description, inputSatsText]);
}, [description, satsAmount, t]);

const renderSteps = () => {
switch (step) {
Expand All @@ -142,16 +223,25 @@ export function Receive() {
<Grid col="1">
<Column>
<h1 className={styles.title}>{t('lightning.receive.subtitle')}</h1>
<span>{t('lightning.receive.amountSats.label')} ({<AmountWithUnit alwaysShowAmounts amount={invoiceAmount || { amount: '', unit: 'sat', estimated: true }} enableRotateUnit convertToFiat/>})</span>
<Input
type="number"
<NumberInput
step="1"
min="0"
label={t('lightning.receive.amountSats.label')}
placeholder={t('lightning.receive.amountSats.placeholder')}
id="amountSatsInput"
onInput={onAmountSatsChange}
onChange={onAmountSatsChange}
value={inputSatsText}
autoFocus
/>
<NumberInput
step="any"
min="0"
label={defaultCurrency}
placeholder={t('lightning.receive.amountSats.placeholder')}
id="amountFiatInput"
onChange={onAmountFiatChange}
value={inputFiatText}
/>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
<Input
label={t('lightning.receive.description.label')}
placeholder={t('lightning.receive.description.placeholder')}
Expand All @@ -164,7 +254,7 @@ export function Receive() {
</Grid>
</ViewContent>
<ViewButtons>
<Button primary onClick={receivePayment} disabled={disableConfirm}>
<Button primary onClick={receivePayment} disabled={satsAmount === undefined}>
{t('lightning.receive.invoice.create')}
</Button>
<Button secondary onClick={back}>
Expand Down