From 8c1567bc6f11b7d337b092ab9f3b40bddd495111 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Sat, 16 Mar 2024 15:36:58 +0100 Subject: [PATCH 01/17] feat: dual currency input supports fiat input --- src/app/components/BudgetControl/index.tsx | 6 +- src/app/components/SitePreferences/index.tsx | 20 +- .../form/DualCurrencyField/index.test.tsx | 2 +- .../form/DualCurrencyField/index.tsx | 171 ++++++++++++++++-- src/app/context/SettingsContext.tsx | 6 +- src/app/screens/ConfirmKeysend/index.tsx | 10 +- src/app/screens/ConfirmPayment/index.tsx | 12 +- src/app/screens/Keysend/index.tsx | 23 +-- src/app/screens/LNURLPay/index.tsx | 22 +-- src/app/screens/LNURLWithdraw/index.tsx | 24 +-- src/app/screens/MakeInvoice/index.tsx | 20 +- src/app/screens/ReceiveInvoice/index.tsx | 19 +- .../screens/SendToBitcoinAddress/index.tsx | 26 ++- 13 files changed, 213 insertions(+), 148 deletions(-) diff --git a/src/app/components/BudgetControl/index.tsx b/src/app/components/BudgetControl/index.tsx index bf684228da..453ab63972 100644 --- a/src/app/components/BudgetControl/index.tsx +++ b/src/app/components/BudgetControl/index.tsx @@ -9,8 +9,8 @@ type Props = { onRememberChange: ChangeEventHandler; budget: string; onBudgetChange: ChangeEventHandler; - fiatAmount: string; disabled?: boolean; + showFiat?: boolean; }; function BudgetControl({ @@ -18,8 +18,8 @@ function BudgetControl({ onRememberChange, budget, onBudgetChange, - fiatAmount, disabled = false, + showFiat = false, }: Props) { const { t } = useTranslation("components", { keyPrefix: "budget_control", @@ -60,8 +60,8 @@ function BudgetControl({
{ - if (budget !== "" && showFiat) { - const getFiat = async () => { - const res = await getFormattedFiat(budget); - setFiatAmount(res); - }; - - getFiat(); - } - }, [budget, showFiat, getFormattedFiat]); - function openModal() { setBudget(allowance.totalBudget.toString()); setLnurlAuth(allowance.lnurlAuth); @@ -196,7 +180,7 @@ function SitePreferences({ launcherType, allowance, onEdit, onDelete }: Props) { placeholder={tCommon("sats", { count: 0 })} value={budget} hint={t("hint")} - fiatValue={fiatAmount} + showFiat={showFiat} onChange={(e) => setBudget(e.target.value)} />
diff --git a/src/app/components/form/DualCurrencyField/index.test.tsx b/src/app/components/form/DualCurrencyField/index.test.tsx index ff8a4909e6..e25eca8baa 100644 --- a/src/app/components/form/DualCurrencyField/index.test.tsx +++ b/src/app/components/form/DualCurrencyField/index.test.tsx @@ -5,7 +5,7 @@ import type { Props } from "./index"; import DualCurrencyField from "./index"; const props: Props = { - fiatValue: "$10.00", + showFiat: true, label: "Amount", }; diff --git a/src/app/components/form/DualCurrencyField/index.tsx b/src/app/components/form/DualCurrencyField/index.tsx index d7d443992e..d5a43b4f95 100644 --- a/src/app/components/form/DualCurrencyField/index.tsx +++ b/src/app/components/form/DualCurrencyField/index.tsx @@ -1,22 +1,35 @@ -import { useEffect, useRef } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; +import { useAccount } from "~/app/context/AccountContext"; +import { useSettings } from "~/app/context/SettingsContext"; import { classNames } from "~/app/utils"; - import { RangeLabel } from "./rangeLabel"; +export type DualCurrencyFieldChangeEvent = + React.ChangeEvent & { + target: HTMLInputElement & { + valueInFiat: number; + formattedValueInFiat: string; + valueInSats: number; + formattedValueInSats: string; + }; + }; + export type Props = { suffix?: string; endAdornment?: React.ReactNode; - fiatValue: string; label: string; hint?: string; amountExceeded?: boolean; rangeExceeded?: boolean; + baseToAltRate?: number; + showFiat?: boolean; + onChange?: (e: DualCurrencyFieldChangeEvent) => void; }; export default function DualCurrencyField({ label, - fiatValue, + showFiat = true, id, placeholder, required = false, @@ -38,10 +51,140 @@ export default function DualCurrencyField({ rangeExceeded, }: React.InputHTMLAttributes & Props) { const { t: tCommon } = useTranslation("common"); + const { getFormattedInCurrency, getCurrencyRate, settings } = useSettings(); + const { account } = useAccount(); + const inputEl = useRef(null); const outerStyles = "rounded-md border border-gray-300 dark:border-gray-800 bg-white dark:bg-black transition duration-300"; + const initialized = useRef(false); + const [useFiatAsMain, _setUseFiatAsMain] = useState(false); + const [altFormattedValue, setAltFormattedValue] = useState(""); + const [minValue, setMinValue] = useState(min); + const [maxValue, setMaxValue] = useState(max); + const [inputValue, setInputValue] = useState(value || 0); + + const getValues = useCallback( + async (value: number, useFiatAsMain: boolean) => { + let valueInSats = Number(value); + let valueInFiat = 0; + + if (showFiat) { + valueInFiat = Number(value); + const rate = await getCurrencyRate(); + if (useFiatAsMain) { + valueInSats = Math.round(valueInSats / rate); + } else { + valueInFiat = Math.round(valueInFiat * rate * 100) / 100.0; + } + } + + const formattedSats = getFormattedInCurrency(valueInSats, "BTC"); + let formattedFiat = ""; + + if (showFiat && valueInFiat) { + formattedFiat = getFormattedInCurrency(valueInFiat, settings.currency); + } + + return { + valueInSats, + formattedSats, + valueInFiat, + formattedFiat, + }; + }, + [getCurrencyRate, getFormattedInCurrency, showFiat, settings.currency] + ); + + useEffect(() => { + (async () => { + if (showFiat) { + const { formattedSats, formattedFiat } = await getValues( + Number(inputValue), + useFiatAsMain + ); + setAltFormattedValue(useFiatAsMain ? formattedSats : formattedFiat); + } + })(); + }, [useFiatAsMain, inputValue, getValues, showFiat]); + + const setUseFiatAsMain = useCallback( + async (v: boolean) => { + if (!showFiat) v = false; + + const rate = showFiat ? await getCurrencyRate() : 1; + if (min) { + let minV; + if (v) { + minV = (Math.round(Number(min) * rate * 100) / 100.0).toString(); + } else { + minV = min; + } + + setMinValue(minV); + } + if (max) { + let maxV; + if (v) { + maxV = (Math.round(Number(max) * rate * 100) / 100.0).toString(); + } else { + maxV = max; + } + + setMaxValue(maxV); + } + + let newValue; + if (v) { + newValue = Math.round(Number(inputValue) * rate * 100) / 100.0; + } else { + newValue = Math.round(Number(inputValue) / rate); + } + + _setUseFiatAsMain(v); + setInputValue(newValue); + }, + [showFiat, getCurrencyRate, inputValue, min, max] + ); + + const swapCurrencies = () => { + setUseFiatAsMain(!useFiatAsMain); + }; + + const onChangeWrapper = useCallback( + async (e: React.ChangeEvent) => { + setInputValue(e.target.value); + + if (onChange) { + const value = Number(e.target.value); + const { valueInSats, formattedSats, valueInFiat, formattedFiat } = + await getValues(value, useFiatAsMain); + const newEvent: DualCurrencyFieldChangeEvent = { + ...e, + target: { + ...e.target, + value: valueInSats.toString(), + valueInFiat, + formattedValueInFiat: formattedFiat, + valueInSats, + formattedValueInSats: formattedSats, + }, + }; + onChange(newEvent); + } + }, + [onChange, useFiatAsMain, getValues] + ); + + // default to fiat when account currency is set to anything other than BTC + useEffect(() => { + if (!initialized.current) { + setUseFiatAsMain(!!(account?.currency && account?.currency !== "BTC")); + initialized.current = true; + } + }, [account?.currency, setUseFiatAsMain]); + const inputNode = ( ); @@ -90,14 +234,15 @@ export default function DualCurrencyField({ > {label} - {(min || max) && ( + {(minValue || maxValue) && ( - {tCommon("sats_other")} + {" "} + {useFiatAsMain ? "" : tCommon("sats_other")} )} @@ -114,9 +259,9 @@ export default function DualCurrencyField({ > {inputNode} - {!!fiatValue && ( -

- ~{fiatValue} + {!!altFormattedValue && ( +

+ ~{altFormattedValue}

)} diff --git a/src/app/context/SettingsContext.tsx b/src/app/context/SettingsContext.tsx index 64ced88319..5dfe59ff42 100644 --- a/src/app/context/SettingsContext.tsx +++ b/src/app/context/SettingsContext.tsx @@ -23,8 +23,9 @@ interface SettingsContextType { getFormattedNumber: (amount: number | string) => string; getFormattedInCurrency: ( amount: number | string, - currency?: ACCOUNT_CURRENCIES + currency?: ACCOUNT_CURRENCIES | CURRENCIES ) => string; + getCurrencyRate: () => Promise; } type Setting = Partial; @@ -115,7 +116,7 @@ export const SettingsProvider = ({ const getFormattedInCurrency = ( amount: number | string, - currency = "BTC" as ACCOUNT_CURRENCIES + currency = "BTC" as ACCOUNT_CURRENCIES | CURRENCIES ) => { if (currency === "BTC") { return getFormattedSats(amount); @@ -149,6 +150,7 @@ export const SettingsProvider = ({ getFormattedSats, getFormattedNumber, getFormattedInCurrency, + getCurrencyRate, settings, updateSetting, isLoading, diff --git a/src/app/screens/ConfirmKeysend/index.tsx b/src/app/screens/ConfirmKeysend/index.tsx index 34c3de533d..1b7b9217ad 100644 --- a/src/app/screens/ConfirmKeysend/index.tsx +++ b/src/app/screens/ConfirmKeysend/index.tsx @@ -41,7 +41,6 @@ function ConfirmKeysend() { ((parseInt(amount) || 0) * 10).toString() ); const [fiatAmount, setFiatAmount] = useState(""); - const [fiatBudgetAmount, setFiatBudgetAmount] = useState(""); const [loading, setLoading] = useState(false); const [successMessage, setSuccessMessage] = useState(""); @@ -54,13 +53,6 @@ function ConfirmKeysend() { })(); }, [amount, showFiat, getFormattedFiat]); - useEffect(() => { - (async () => { - const res = await getFormattedFiat(budget); - setFiatBudgetAmount(res); - })(); - }, [budget, showFiat, getFormattedFiat]); - async function confirm() { if (rememberMe && budget) { await saveBudget(); @@ -153,7 +145,7 @@ function ConfirmKeysend() {
{ setRememberMe(event.target.checked); diff --git a/src/app/screens/ConfirmPayment/index.tsx b/src/app/screens/ConfirmPayment/index.tsx index de55ee54cb..91665ed904 100644 --- a/src/app/screens/ConfirmPayment/index.tsx +++ b/src/app/screens/ConfirmPayment/index.tsx @@ -44,7 +44,6 @@ function ConfirmPayment() { ((invoice.satoshis || 0) * 10).toString() ); const [fiatAmount, setFiatAmount] = useState(""); - const [fiatBudgetAmount, setFiatBudgetAmount] = useState(""); const formattedInvoiceSats = getFormattedSats(invoice.satoshis || 0); @@ -57,15 +56,6 @@ function ConfirmPayment() { })(); }, [invoice.satoshis, showFiat, getFormattedFiat]); - useEffect(() => { - (async () => { - if (showFiat && budget) { - const res = await getFormattedFiat(budget); - setFiatBudgetAmount(res); - } - })(); - }, [budget, showFiat, getFormattedFiat]); - const [rememberMe, setRememberMe] = useState(false); const [loading, setLoading] = useState(false); const [successMessage, setSuccessMessage] = useState(""); @@ -160,7 +150,7 @@ function ConfirmPayment() {
{navState.origin && ( { setRememberMe(event.target.checked); diff --git a/src/app/screens/Keysend/index.tsx b/src/app/screens/Keysend/index.tsx index d581e0359a..3e02645801 100644 --- a/src/app/screens/Keysend/index.tsx +++ b/src/app/screens/Keysend/index.tsx @@ -5,9 +5,11 @@ import Header from "@components/Header"; import IconButton from "@components/IconButton"; import ResultCard from "@components/ResultCard"; import SatButtons from "@components/SatButtons"; -import DualCurrencyField from "@components/form/DualCurrencyField"; +import DualCurrencyField, { + DualCurrencyFieldChangeEvent, +} from "@components/form/DualCurrencyField"; import { PopiconsChevronLeftLine } from "@popicons/react"; -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import Container from "~/app/components/Container"; @@ -21,7 +23,6 @@ function Keysend() { const { isLoading: isLoadingSettings, settings, - getFormattedFiat, getFormattedSats, } = useSettings(); const showFiat = !isLoadingSettings && settings.showFiat; @@ -46,15 +47,6 @@ function Keysend() { : +amountSat > (auth?.account?.balance || 0); const rangeExceeded = +amountSat < amountMin; - useEffect(() => { - (async () => { - if (amountSat !== "" && showFiat) { - const res = await getFormattedFiat(amountSat); - setFiatAmount(res); - } - })(); - }, [amountSat, showFiat, getFormattedFiat]); - async function confirm() { try { setLoading(true); @@ -126,9 +118,12 @@ function Keysend() { id="amount" label={t("amount.label")} min={1} - onChange={(e) => setAmountSat(e.target.value)} value={amountSat} - fiatValue={fiatAmount} + showFiat={showFiat} + onChange={(e: DualCurrencyFieldChangeEvent) => { + setAmountSat(e.target.value); + setFiatAmount(e.target.formattedValueInFiat); + }} hint={`${tCommon("balance")}: ${auth?.balancesDecorated ?.accountBalance}`} amountExceeded={amountExceeded} diff --git a/src/app/screens/LNURLPay/index.tsx b/src/app/screens/LNURLPay/index.tsx index 27b212a53a..e10ae1f551 100644 --- a/src/app/screens/LNURLPay/index.tsx +++ b/src/app/screens/LNURLPay/index.tsx @@ -5,7 +5,9 @@ import Hyperlink from "@components/Hyperlink"; import PublisherCard from "@components/PublisherCard"; import ResultCard from "@components/ResultCard"; import SatButtons from "@components/SatButtons"; -import DualCurrencyField from "@components/form/DualCurrencyField"; +import DualCurrencyField, { + DualCurrencyFieldChangeEvent, +} from "@components/form/DualCurrencyField"; import TextField from "@components/form/TextField"; import { PopiconsChevronBottomLine, @@ -35,7 +37,6 @@ import type { LNURLPaymentSuccessAction, PaymentResponse, } from "~/types"; - const Dt = ({ children }: { children: React.ReactNode }) => (
{children}
); @@ -53,7 +54,6 @@ function LNURLPay() { const { isLoading: isLoadingSettings, settings, - getFormattedFiat, getFormattedSats, } = useSettings(); const showFiat = !isLoadingSettings && settings.showFiat; @@ -87,15 +87,6 @@ function LNURLPay() { LNURLPaymentSuccessAction | undefined >(); - useEffect(() => { - const getFiat = async () => { - const res = await getFormattedFiat(valueSat); - setFiatValue(res); - }; - - getFiat(); - }, [valueSat, showFiat, getFormattedFiat]); - useEffect(() => { !!settings.userName && setUserName(settings.userName); !!settings.userEmail && setUserEmail(settings.userEmail); @@ -451,8 +442,11 @@ function LNURLPay() { max={amountMax} rangeExceeded={rangeExceeded} value={valueSat} - onChange={(e) => setValueSat(e.target.value)} - fiatValue={fiatValue} + onChange={(e: DualCurrencyFieldChangeEvent) => { + setValueSat(e.target.value); + setFiatValue(e.target.formattedValueInFiat); + }} + showFiat={showFiat} hint={`${tCommon("balance")}: ${auth ?.balancesDecorated?.accountBalance}`} amountExceeded={amountExceeded} diff --git a/src/app/screens/LNURLWithdraw/index.tsx b/src/app/screens/LNURLWithdraw/index.tsx index 3deebfb9ab..e26d9c7863 100644 --- a/src/app/screens/LNURLWithdraw/index.tsx +++ b/src/app/screens/LNURLWithdraw/index.tsx @@ -4,9 +4,11 @@ import Container from "@components/Container"; import ContentMessage from "@components/ContentMessage"; import PublisherCard from "@components/PublisherCard"; import ResultCard from "@components/ResultCard"; -import DualCurrencyField from "@components/form/DualCurrencyField"; +import DualCurrencyField, { + DualCurrencyFieldChangeEvent, +} from "@components/form/DualCurrencyField"; import axios from "axios"; -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import ScreenHeader from "~/app/components/ScreenHeader"; @@ -17,7 +19,6 @@ import { USER_REJECTED_ERROR } from "~/common/constants"; import api from "~/common/lib/api"; import msg from "~/common/lib/msg"; import type { LNURLWithdrawServiceResponse } from "~/types"; - function LNURLWithdraw() { const { t } = useTranslation("translation", { keyPrefix: "lnurlwithdraw" }); const { t: tCommon } = useTranslation("common"); @@ -27,7 +28,6 @@ function LNURLWithdraw() { const { isLoading: isLoadingSettings, settings, - getFormattedFiat, getFormattedSats, } = useSettings(); const showFiat = !isLoadingSettings && settings.showFiat; @@ -43,15 +43,6 @@ function LNURLWithdraw() { const [successMessage, setSuccessMessage] = useState(""); const [fiatValue, setFiatValue] = useState(""); - useEffect(() => { - if (valueSat !== "" && showFiat) { - (async () => { - const res = await getFormattedFiat(valueSat); - setFiatValue(res); - })(); - } - }, [valueSat, showFiat, getFormattedFiat]); - async function confirm() { try { setLoadingConfirm(true); @@ -117,8 +108,11 @@ function LNURLWithdraw() { min={Math.floor(minWithdrawable / 1000)} max={Math.floor(maxWithdrawable / 1000)} value={valueSat} - onChange={(e) => setValueSat(e.target.value)} - fiatValue={fiatValue} + onChange={(e: DualCurrencyFieldChangeEvent) => { + setValueSat(e.target.value); + setFiatValue(e.target.formattedValueInFiat); + }} + showFiat={showFiat} />
); diff --git a/src/app/screens/MakeInvoice/index.tsx b/src/app/screens/MakeInvoice/index.tsx index cf0062e7ad..f1d7bb8e59 100644 --- a/src/app/screens/MakeInvoice/index.tsx +++ b/src/app/screens/MakeInvoice/index.tsx @@ -4,7 +4,7 @@ import PublisherCard from "@components/PublisherCard"; import SatButtons from "@components/SatButtons"; import DualCurrencyField from "@components/form/DualCurrencyField"; import TextField from "@components/form/TextField"; -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import ScreenHeader from "~/app/components/ScreenHeader"; import toast from "~/app/components/Toast"; @@ -25,11 +25,7 @@ const Dd = ({ children }: { children: React.ReactNode }) => ( function MakeInvoice() { const navState = useNavigationState(); - const { - isLoading: isLoadingSettings, - settings, - getFormattedFiat, - } = useSettings(); + const { isLoading: isLoadingSettings, settings } = useSettings(); const showFiat = !isLoadingSettings && settings.showFiat; const origin = navState.origin as OriginData; @@ -39,7 +35,6 @@ function MakeInvoice() { const memoEditable = navState.args?.memoEditable; const [loading, setLoading] = useState(false); const [valueSat, setValueSat] = useState(invoiceAttributes.amount || ""); - const [fiatValue, setFiatValue] = useState(""); const [memo, setMemo] = useState(invoiceAttributes.memo || ""); const [error, setError] = useState(""); const { t: tCommon } = useTranslation("common"); @@ -47,15 +42,6 @@ function MakeInvoice() { keyPrefix: "make_invoice", }); - useEffect(() => { - if (valueSat !== "" && showFiat) { - (async () => { - const res = await getFormattedFiat(valueSat); - setFiatValue(res); - })(); - } - }, [valueSat, showFiat, getFormattedFiat]); - function handleValueChange(amount: string) { setError(""); if ( @@ -132,7 +118,7 @@ function MakeInvoice() { max={invoiceAttributes.maximumAmount} value={valueSat} onChange={(e) => handleValueChange(e.target.value)} - fiatValue={fiatValue} + showFiat={showFiat} />
diff --git a/src/app/screens/ReceiveInvoice/index.tsx b/src/app/screens/ReceiveInvoice/index.tsx index a1799106d1..acca2ab255 100644 --- a/src/app/screens/ReceiveInvoice/index.tsx +++ b/src/app/screens/ReceiveInvoice/index.tsx @@ -24,11 +24,7 @@ function ReceiveInvoice() { const { t: tCommon } = useTranslation("common"); const auth = useAccount(); - const { - isLoading: isLoadingSettings, - settings, - getFormattedFiat, - } = useSettings(); + const { isLoading: isLoadingSettings, settings } = useSettings(); const showFiat = !isLoadingSettings && settings.showFiat; const navigate = useNavigate(); @@ -56,17 +52,6 @@ function ReceiveInvoice() { }; }, []); - const [fiatAmount, setFiatAmount] = useState(""); - - useEffect(() => { - if (formData.amount !== "" && showFiat) { - (async () => { - const res = await getFormattedFiat(formData.amount); - setFiatAmount(res); - })(); - } - }, [formData, showFiat, getFormattedFiat]); - function handleChange( event: React.ChangeEvent ) { @@ -242,7 +227,7 @@ function ReceiveInvoice() { min={0} label={t("amount.label")} placeholder={t("amount.placeholder")} - fiatValue={fiatAmount} + showFiat={showFiat} onChange={handleChange} autoFocus /> diff --git a/src/app/screens/SendToBitcoinAddress/index.tsx b/src/app/screens/SendToBitcoinAddress/index.tsx index b363d39bf3..7dc46234cb 100644 --- a/src/app/screens/SendToBitcoinAddress/index.tsx +++ b/src/app/screens/SendToBitcoinAddress/index.tsx @@ -1,11 +1,15 @@ -import { PopiconsLinkExternalSolid } from "@popicons/react"; import Button from "@components/Button"; import ConfirmOrCancel from "@components/ConfirmOrCancel"; import Header from "@components/Header"; import IconButton from "@components/IconButton"; -import DualCurrencyField from "@components/form/DualCurrencyField"; +import DualCurrencyField, { + DualCurrencyFieldChangeEvent, +} from "@components/form/DualCurrencyField"; import { CreateSwapResponse } from "@getalby/sdk/dist/types"; -import { PopiconsChevronLeftLine } from "@popicons/react"; +import { + PopiconsChevronLeftLine, + PopiconsLinkExternalSolid, +} from "@popicons/react"; import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import Skeleton from "react-loading-skeleton"; @@ -65,15 +69,6 @@ function SendToBitcoinAddress() { }); const { t: tCommon } = useTranslation("common"); - useEffect(() => { - (async () => { - if (amountSat !== "" && showFiat) { - const res = await getFormattedFiat(amountSat); - setFiatAmount(res); - } - })(); - }, [amountSat, showFiat, getFormattedFiat]); - useEffect(() => { (async () => { try { @@ -255,9 +250,12 @@ function SendToBitcoinAddress() { label={tCommon("amount")} min={amountMin} max={amountMax} - onChange={(e) => setAmountSat(e.target.value)} + onChange={(e: DualCurrencyFieldChangeEvent) => { + setAmountSat(e.target.value); + setFiatAmount(e.target.formattedValueInFiat); + }} + showFiat={showFiat} value={amountSat} - fiatValue={fiatAmount} rangeExceeded={rangeExceeded} amountExceeded={amountExceeded} hint={`${tCommon("balance")}: ${auth?.balancesDecorated From 87a8edc7cce25447c7e46e865c8366f2b1d3ec66 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Sat, 23 Mar 2024 14:56:21 +0100 Subject: [PATCH 02/17] feat: prefix dual currency input with main symbol, add currency to input placeholder --- src/app/components/BudgetControl/index.tsx | 3 -- src/app/components/SitePreferences/index.tsx | 1 - .../form/DualCurrencyField/index.tsx | 48 ++++++++++++++++--- src/app/context/SettingsContext.tsx | 10 ++++ src/app/screens/ReceiveInvoice/index.tsx | 1 - src/common/utils/currencyConvert.ts | 16 +++++++ src/i18n/locales/en/translation.json | 1 + 7 files changed, 68 insertions(+), 12 deletions(-) diff --git a/src/app/components/BudgetControl/index.tsx b/src/app/components/BudgetControl/index.tsx index 453ab63972..c32e5e0040 100644 --- a/src/app/components/BudgetControl/index.tsx +++ b/src/app/components/BudgetControl/index.tsx @@ -25,8 +25,6 @@ function BudgetControl({ keyPrefix: "budget_control", }); - const { t: tCommon } = useTranslation("common"); - return (
@@ -65,7 +63,6 @@ function BudgetControl({ id="budget" min={0} label={t("budget.label")} - placeholder={tCommon("sats", { count: 0 })} value={budget} onChange={onBudgetChange} /> diff --git a/src/app/components/SitePreferences/index.tsx b/src/app/components/SitePreferences/index.tsx index cf5b228c89..4dda52b05f 100644 --- a/src/app/components/SitePreferences/index.tsx +++ b/src/app/components/SitePreferences/index.tsx @@ -177,7 +177,6 @@ function SitePreferences({ launcherType, allowance, onEdit, onDelete }: Props) { label={t("new_budget.label")} min={0} autoFocus - placeholder={tCommon("sats", { count: 0 })} value={budget} hint={t("hint")} showFiat={showFiat} diff --git a/src/app/components/form/DualCurrencyField/index.tsx b/src/app/components/form/DualCurrencyField/index.tsx index d5a43b4f95..d055e59a94 100644 --- a/src/app/components/form/DualCurrencyField/index.tsx +++ b/src/app/components/form/DualCurrencyField/index.tsx @@ -51,7 +51,12 @@ export default function DualCurrencyField({ rangeExceeded, }: React.InputHTMLAttributes & Props) { const { t: tCommon } = useTranslation("common"); - const { getFormattedInCurrency, getCurrencyRate, settings } = useSettings(); + const { + getFormattedInCurrency, + getCurrencyRate, + getCurrencySymbol, + settings, + } = useSettings(); const { account } = useAccount(); const inputEl = useRef(null); @@ -64,6 +69,8 @@ export default function DualCurrencyField({ const [minValue, setMinValue] = useState(min); const [maxValue, setMaxValue] = useState(max); const [inputValue, setInputValue] = useState(value || 0); + const [inputPrefix, setInputPrefix] = useState(""); + const [inputPlaceHolder, setInputPlaceHolder] = useState(placeholder || ""); const getValues = useCallback( async (value: number, useFiatAsMain: boolean) => { @@ -83,7 +90,7 @@ export default function DualCurrencyField({ const formattedSats = getFormattedInCurrency(valueInSats, "BTC"); let formattedFiat = ""; - if (showFiat && valueInFiat) { + if (showFiat) { formattedFiat = getFormattedInCurrency(valueInFiat, settings.currency); } @@ -144,8 +151,26 @@ export default function DualCurrencyField({ _setUseFiatAsMain(v); setInputValue(newValue); + setInputPrefix(getCurrencySymbol(v ? settings.currency : "BTC")); + if (!placeholder) { + setInputPlaceHolder( + tCommon("amount_placeholder", { + currency: v ? settings.currency : "Satoshis", + }) + ); + } }, - [showFiat, getCurrencyRate, inputValue, min, max] + [ + showFiat, + getCurrencyRate, + inputValue, + min, + max, + settings.currency, + tCommon, + getCurrencySymbol, + placeholder, + ] ); const swapCurrencies = () => { @@ -196,14 +221,14 @@ export default function DualCurrencyField({ "block w-full placeholder-gray-500 dark:placeholder-gray-600 dark:text-white ", "px-0 border-0 focus:ring-0 bg-transparent" )} - placeholder={placeholder} + placeholder={inputPlaceHolder} required={required} pattern={pattern} title={title} onChange={onChangeWrapper} onFocus={onFocus} onBlur={onBlur} - value={inputValue} + value={inputValue ? inputValue : undefined} autoFocus={autoFocus} autoComplete={autoComplete} disabled={disabled} @@ -257,17 +282,26 @@ export default function DualCurrencyField({ outerStyles )} > + {!!inputPrefix && ( +

+ {inputPrefix} +

+ )} + {inputNode} {!!altFormattedValue && ( -

+

~{altFormattedValue}

)} {suffix && ( { inputEl.current?.focus(); }} diff --git a/src/app/context/SettingsContext.tsx b/src/app/context/SettingsContext.tsx index 5dfe59ff42..52db131475 100644 --- a/src/app/context/SettingsContext.tsx +++ b/src/app/context/SettingsContext.tsx @@ -7,6 +7,7 @@ import { ACCOUNT_CURRENCIES, CURRENCIES } from "~/common/constants"; import api from "~/common/lib/api"; import { DEFAULT_SETTINGS } from "~/common/settings"; import { + getCurrencySymbol as getCurrencySymbolUtil, getFormattedCurrency as getFormattedCurrencyUtil, getFormattedFiat as getFormattedFiatUtil, getFormattedNumber as getFormattedNumberUtil, @@ -21,6 +22,7 @@ interface SettingsContextType { getFormattedFiat: (amount: number | string) => Promise; getFormattedSats: (amount: number | string) => string; getFormattedNumber: (amount: number | string) => string; + getCurrencySymbol: (currency: CURRENCIES | ACCOUNT_CURRENCIES) => string; getFormattedInCurrency: ( amount: number | string, currency?: ACCOUNT_CURRENCIES | CURRENCIES @@ -129,6 +131,13 @@ export const SettingsProvider = ({ }); }; + const getCurrencySymbol = (currency: CURRENCIES | ACCOUNT_CURRENCIES) => { + return getCurrencySymbolUtil({ + currency, + locale: settings.locale, + }); + }; + // update locale on every change useEffect(() => { i18n.changeLanguage(settings.locale); @@ -151,6 +160,7 @@ export const SettingsProvider = ({ getFormattedNumber, getFormattedInCurrency, getCurrencyRate, + getCurrencySymbol, settings, updateSetting, isLoading, diff --git a/src/app/screens/ReceiveInvoice/index.tsx b/src/app/screens/ReceiveInvoice/index.tsx index acca2ab255..ebe052012b 100644 --- a/src/app/screens/ReceiveInvoice/index.tsx +++ b/src/app/screens/ReceiveInvoice/index.tsx @@ -226,7 +226,6 @@ function ReceiveInvoice() { id="amount" min={0} label={t("amount.label")} - placeholder={t("amount.placeholder")} showFiat={showFiat} onChange={handleChange} autoFocus diff --git a/src/common/utils/currencyConvert.ts b/src/common/utils/currencyConvert.ts index 2a83b657d5..b792081002 100644 --- a/src/common/utils/currencyConvert.ts +++ b/src/common/utils/currencyConvert.ts @@ -25,6 +25,22 @@ export const getFormattedCurrency = (params: { }).format(Number(params.amount)); }; +export const getCurrencySymbol = (params: { + currency: CURRENCIES | ACCOUNT_CURRENCIES; + locale: string; +}) => { + if (params.currency === "BTC") return "₿"; + const l = (params.locale || "en").toLowerCase().replace("_", "-"); + const value = + new Intl.NumberFormat(l || "en", { + style: "currency", + currency: params.currency, + }) + .formatToParts(0) + .find((part) => part.type === "currency")?.value || ""; + return value; +}; + export const getFormattedFiat = (params: { amount: number | string; rate: number; diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index a5ad97db95..12dce528b0 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -988,6 +988,7 @@ "description": "Description", "description_full": "Full Description", "success_message": "{{amount}}{{fiatAmount}} are on their way to {{destination}}", + "amount_placeholder": "Amount in {{currency}}...", "response": "Response", "message": "Message", "help": "Alby Guides", From 3bffba9ce1ca63d4d540ba1a2f702ae336cb60c5 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Mon, 1 Apr 2024 20:12:13 +0200 Subject: [PATCH 03/17] fix: amount in Satoshis -> amount in sats --- src/app/components/form/DualCurrencyField/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/form/DualCurrencyField/index.tsx b/src/app/components/form/DualCurrencyField/index.tsx index d055e59a94..f191de4aa6 100644 --- a/src/app/components/form/DualCurrencyField/index.tsx +++ b/src/app/components/form/DualCurrencyField/index.tsx @@ -155,7 +155,7 @@ export default function DualCurrencyField({ if (!placeholder) { setInputPlaceHolder( tCommon("amount_placeholder", { - currency: v ? settings.currency : "Satoshis", + currency: v ? settings.currency : "sats", }) ); } From 29c49b46c477b7ec9ecca0af55a4a1a940883d8e Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Mon, 1 Apr 2024 20:28:34 +0200 Subject: [PATCH 04/17] fix: attempt at making unit tests happy --- src/app/components/form/DualCurrencyField/index.tsx | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/app/components/form/DualCurrencyField/index.tsx b/src/app/components/form/DualCurrencyField/index.tsx index f191de4aa6..9533a64db5 100644 --- a/src/app/components/form/DualCurrencyField/index.tsx +++ b/src/app/components/form/DualCurrencyField/index.tsx @@ -72,6 +72,8 @@ export default function DualCurrencyField({ const [inputPrefix, setInputPrefix] = useState(""); const [inputPlaceHolder, setInputPlaceHolder] = useState(placeholder || ""); + const userCurrency = settings?.currency || "BTC"; + const getValues = useCallback( async (value: number, useFiatAsMain: boolean) => { let valueInSats = Number(value); @@ -91,7 +93,7 @@ export default function DualCurrencyField({ let formattedFiat = ""; if (showFiat) { - formattedFiat = getFormattedInCurrency(valueInFiat, settings.currency); + formattedFiat = getFormattedInCurrency(valueInFiat, userCurrency); } return { @@ -101,7 +103,7 @@ export default function DualCurrencyField({ formattedFiat, }; }, - [getCurrencyRate, getFormattedInCurrency, showFiat, settings.currency] + [getCurrencyRate, getFormattedInCurrency, showFiat, userCurrency] ); useEffect(() => { @@ -151,11 +153,11 @@ export default function DualCurrencyField({ _setUseFiatAsMain(v); setInputValue(newValue); - setInputPrefix(getCurrencySymbol(v ? settings.currency : "BTC")); + setInputPrefix(getCurrencySymbol(v ? userCurrency : "BTC")); if (!placeholder) { setInputPlaceHolder( tCommon("amount_placeholder", { - currency: v ? settings.currency : "sats", + currency: v ? userCurrency : "sats", }) ); } @@ -166,7 +168,7 @@ export default function DualCurrencyField({ inputValue, min, max, - settings.currency, + userCurrency, tCommon, getCurrencySymbol, placeholder, From 0394bb771e3f64cdb276c5427101bace6fa2a870 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Tue, 2 Apr 2024 09:15:53 +0200 Subject: [PATCH 05/17] fix: fix some unit tests --- .../form/DualCurrencyField/index.tsx | 42 +++++++------------ src/app/screens/ConfirmKeysend/index.test.tsx | 1 + src/app/screens/ConfirmPayment/index.test.tsx | 1 + src/app/screens/LNURLPay/index.test.tsx | 3 ++ src/app/screens/MakeInvoice/index.test.tsx | 5 ++- 5 files changed, 23 insertions(+), 29 deletions(-) diff --git a/src/app/components/form/DualCurrencyField/index.tsx b/src/app/components/form/DualCurrencyField/index.tsx index 9533a64db5..f7531739b3 100644 --- a/src/app/components/form/DualCurrencyField/index.tsx +++ b/src/app/components/form/DualCurrencyField/index.tsx @@ -90,11 +90,9 @@ export default function DualCurrencyField({ } const formattedSats = getFormattedInCurrency(valueInSats, "BTC"); - let formattedFiat = ""; - - if (showFiat) { - formattedFiat = getFormattedInCurrency(valueInFiat, userCurrency); - } + const formattedFiat = showFiat + ? getFormattedInCurrency(valueInFiat, userCurrency) + : ""; return { valueInSats, @@ -121,35 +119,23 @@ export default function DualCurrencyField({ const setUseFiatAsMain = useCallback( async (v: boolean) => { if (!showFiat) v = false; - const rate = showFiat ? await getCurrencyRate() : 1; - if (min) { - let minV; - if (v) { - minV = (Math.round(Number(min) * rate * 100) / 100.0).toString(); - } else { - minV = min; - } - setMinValue(minV); + if (min) { + setMinValue( + v ? (Math.round(Number(min) * rate * 100) / 100.0).toString() : min + ); } - if (max) { - let maxV; - if (v) { - maxV = (Math.round(Number(max) * rate * 100) / 100.0).toString(); - } else { - maxV = max; - } - setMaxValue(maxV); + if (max) { + setMaxValue( + v ? (Math.round(Number(max) * rate * 100) / 100.0).toString() : max + ); } - let newValue; - if (v) { - newValue = Math.round(Number(inputValue) * rate * 100) / 100.0; - } else { - newValue = Math.round(Number(inputValue) / rate); - } + const newValue = v + ? Math.round(Number(inputValue) * rate * 100) / 100.0 + : Math.round(Number(inputValue) / rate); _setUseFiatAsMain(v); setInputValue(newValue); diff --git a/src/app/screens/ConfirmKeysend/index.test.tsx b/src/app/screens/ConfirmKeysend/index.test.tsx index 618399a1bf..7befe8e43a 100644 --- a/src/app/screens/ConfirmKeysend/index.test.tsx +++ b/src/app/screens/ConfirmKeysend/index.test.tsx @@ -21,6 +21,7 @@ jest.mock("~/app/context/SettingsContext", () => ({ getFormattedNumber: jest.fn(), getFormattedSats: jest.fn(() => "21 sats"), getFormattedFiat: mockGetFiatValue, + getFormattedInCurrency: mockGetFiatValue, }), })); diff --git a/src/app/screens/ConfirmPayment/index.test.tsx b/src/app/screens/ConfirmPayment/index.test.tsx index 36c3ca9a68..39e7acf73b 100644 --- a/src/app/screens/ConfirmPayment/index.test.tsx +++ b/src/app/screens/ConfirmPayment/index.test.tsx @@ -48,6 +48,7 @@ jest.mock("~/app/context/SettingsContext", () => ({ getFormattedFiat: mockGetFiatValue, getFormattedNumber: jest.fn(), getFormattedSats: jest.fn(() => "25 sats"), + getCurrencySymbol: jest.fn(() => "₿"), }), })); diff --git a/src/app/screens/LNURLPay/index.test.tsx b/src/app/screens/LNURLPay/index.test.tsx index 8a3835d1fe..5ed28e25aa 100644 --- a/src/app/screens/LNURLPay/index.test.tsx +++ b/src/app/screens/LNURLPay/index.test.tsx @@ -15,6 +15,9 @@ jest.mock("~/app/context/SettingsContext", () => ({ getFormattedFiat: mockGetFiatValue, getFormattedNumber: jest.fn(), getFormattedSats: jest.fn(), + getCurrencyRate: jest.fn(() => 1), + getCurrencySymbol: jest.fn(() => "₿"), + getFormattedInCurrency: jest.fn(), }), })); diff --git a/src/app/screens/MakeInvoice/index.test.tsx b/src/app/screens/MakeInvoice/index.test.tsx index 1d6b667710..520a0cbb27 100644 --- a/src/app/screens/MakeInvoice/index.test.tsx +++ b/src/app/screens/MakeInvoice/index.test.tsx @@ -1,4 +1,4 @@ -import { render, screen, act } from "@testing-library/react"; +import { act, render, screen } from "@testing-library/react"; import { MemoryRouter } from "react-router-dom"; import { settingsFixture as mockSettings } from "~/../tests/fixtures/settings"; import type { OriginData } from "~/types"; @@ -48,6 +48,9 @@ jest.mock("~/app/context/SettingsContext", () => ({ getFormattedFiat: jest.fn(() => Promise.resolve("$0.01")), getFormattedNumber: jest.fn(), getFormattedSats: jest.fn(), + getCurrencyRate: jest.fn(() => 1), + getCurrencySymbol: jest.fn(() => "₿"), + getFormattedInCurrency: jest.fn(() => "$0.01"), }), })); From 2eb1020634c79a19cfe0ca15bb8b1ac8431bf268 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Wed, 3 Apr 2024 10:50:03 +0200 Subject: [PATCH 06/17] fix: dual currency input event wrapping --- .../form/DualCurrencyField/index.tsx | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/app/components/form/DualCurrencyField/index.tsx b/src/app/components/form/DualCurrencyField/index.tsx index f7531739b3..2a4a6e8e52 100644 --- a/src/app/components/form/DualCurrencyField/index.tsx +++ b/src/app/components/form/DualCurrencyField/index.tsx @@ -173,18 +173,13 @@ export default function DualCurrencyField({ const value = Number(e.target.value); const { valueInSats, formattedSats, valueInFiat, formattedFiat } = await getValues(value, useFiatAsMain); - const newEvent: DualCurrencyFieldChangeEvent = { - ...e, - target: { - ...e.target, - value: valueInSats.toString(), - valueInFiat, - formattedValueInFiat: formattedFiat, - valueInSats, - formattedValueInSats: formattedSats, - }, - }; - onChange(newEvent); + const wrappedEvent: DualCurrencyFieldChangeEvent = + e as DualCurrencyFieldChangeEvent; + wrappedEvent.target.valueInFiat = valueInFiat; + wrappedEvent.target.formattedValueInFiat = formattedFiat; + wrappedEvent.target.valueInSats = valueInSats; + wrappedEvent.target.formattedValueInSats = formattedSats; + onChange(wrappedEvent); } }, [onChange, useFiatAsMain, getValues] From 5a214c964ed0c0de95f7e9d8fa4f26b8bcb06a6d Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Wed, 3 Apr 2024 11:05:22 +0200 Subject: [PATCH 07/17] fix: more unit tests fixing --- src/app/components/SitePreferences/index.test.tsx | 3 +++ .../form/DualCurrencyField/index.test.tsx | 14 ++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/app/components/SitePreferences/index.test.tsx b/src/app/components/SitePreferences/index.test.tsx index 75da970ec8..3b316db003 100644 --- a/src/app/components/SitePreferences/index.test.tsx +++ b/src/app/components/SitePreferences/index.test.tsx @@ -16,6 +16,9 @@ jest.mock("~/app/context/SettingsContext", () => ({ getFormattedFiat: mockGetFiatValue, getFormattedNumber: jest.fn(), getFormattedSats: jest.fn(), + getCurrencyRate: jest.fn(() => 1), + getCurrencySymbol: jest.fn(() => "₿"), + getFormattedInCurrency: mockGetFiatValue, }), })); diff --git a/src/app/components/form/DualCurrencyField/index.test.tsx b/src/app/components/form/DualCurrencyField/index.test.tsx index e25eca8baa..ca4a748b22 100644 --- a/src/app/components/form/DualCurrencyField/index.test.tsx +++ b/src/app/components/form/DualCurrencyField/index.test.tsx @@ -1,5 +1,6 @@ import { render, screen } from "@testing-library/react"; import { MemoryRouter } from "react-router-dom"; +import { settingsFixture as mockSettings } from "~/../tests/fixtures/settings"; import type { Props } from "./index"; import DualCurrencyField from "./index"; @@ -8,6 +9,19 @@ const props: Props = { showFiat: true, label: "Amount", }; +jest.mock("~/app/context/SettingsContext", () => ({ + useSettings: () => ({ + settings: mockSettings, + isLoading: false, + updateSetting: jest.fn(), + getFormattedFiat: jest.fn(() => "$10.00"), + getFormattedNumber: jest.fn(), + getFormattedSats: jest.fn(), + getCurrencyRate: jest.fn(() => 1), + getCurrencySymbol: jest.fn(() => "₿"), + getFormattedInCurrency: jest.fn(() => "$10.00"), + }), +})); describe("DualCurrencyField", () => { test("render", async () => { From 2e2e3299771f846c61600d9e27d9d571f58ecb63 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Wed, 10 Apr 2024 19:10:43 +0200 Subject: [PATCH 08/17] fix: currency toggle issue for default values, improve some naming --- .../form/DualCurrencyField/index.tsx | 68 +++++++++---------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/src/app/components/form/DualCurrencyField/index.tsx b/src/app/components/form/DualCurrencyField/index.tsx index 2a4a6e8e52..e0d555f31b 100644 --- a/src/app/components/form/DualCurrencyField/index.tsx +++ b/src/app/components/form/DualCurrencyField/index.tsx @@ -72,27 +72,23 @@ export default function DualCurrencyField({ const [inputPrefix, setInputPrefix] = useState(""); const [inputPlaceHolder, setInputPlaceHolder] = useState(placeholder || ""); - const userCurrency = settings?.currency || "BTC"; - - const getValues = useCallback( - async (value: number, useFiatAsMain: boolean) => { - let valueInSats = Number(value); + const convertValues = useCallback( + async (inputValue: number, inputInFiat: boolean) => { + const userCurrency = settings?.currency || "BTC"; + let valueInSats = 0; let valueInFiat = 0; + const rate = await getCurrencyRate(); - if (showFiat) { - valueInFiat = Number(value); - const rate = await getCurrencyRate(); - if (useFiatAsMain) { - valueInSats = Math.round(valueInSats / rate); - } else { - valueInFiat = Math.round(valueInFiat * rate * 100) / 100.0; - } + if (inputInFiat) { + valueInFiat = Number(inputValue); + valueInSats = Math.round(valueInFiat / rate); + } else { + valueInSats = Number(inputValue); + valueInFiat = Math.round(valueInSats * rate * 100) / 100.0; } const formattedSats = getFormattedInCurrency(valueInSats, "BTC"); - const formattedFiat = showFiat - ? getFormattedInCurrency(valueInFiat, userCurrency) - : ""; + const formattedFiat = getFormattedInCurrency(valueInFiat, userCurrency); return { valueInSats, @@ -101,25 +97,14 @@ export default function DualCurrencyField({ formattedFiat, }; }, - [getCurrencyRate, getFormattedInCurrency, showFiat, userCurrency] + [getCurrencyRate, getFormattedInCurrency, settings] ); - useEffect(() => { - (async () => { - if (showFiat) { - const { formattedSats, formattedFiat } = await getValues( - Number(inputValue), - useFiatAsMain - ); - setAltFormattedValue(useFiatAsMain ? formattedSats : formattedFiat); - } - })(); - }, [useFiatAsMain, inputValue, getValues, showFiat]); - const setUseFiatAsMain = useCallback( async (v: boolean) => { if (!showFiat) v = false; - const rate = showFiat ? await getCurrencyRate() : 1; + const userCurrency = settings?.currency || "BTC"; + const rate = await getCurrencyRate(); if (min) { setMinValue( @@ -149,12 +134,12 @@ export default function DualCurrencyField({ } }, [ + settings, showFiat, getCurrencyRate, inputValue, min, max, - userCurrency, tCommon, getCurrencySymbol, placeholder, @@ -172,7 +157,7 @@ export default function DualCurrencyField({ if (onChange) { const value = Number(e.target.value); const { valueInSats, formattedSats, valueInFiat, formattedFiat } = - await getValues(value, useFiatAsMain); + await convertValues(value, useFiatAsMain); const wrappedEvent: DualCurrencyFieldChangeEvent = e as DualCurrencyFieldChangeEvent; wrappedEvent.target.valueInFiat = valueInFiat; @@ -182,17 +167,32 @@ export default function DualCurrencyField({ onChange(wrappedEvent); } }, - [onChange, useFiatAsMain, getValues] + [onChange, useFiatAsMain, convertValues] ); // default to fiat when account currency is set to anything other than BTC useEffect(() => { if (!initialized.current) { - setUseFiatAsMain(!!(account?.currency && account?.currency !== "BTC")); + if (account?.currency && account?.currency !== "BTC") { + setUseFiatAsMain(true); + } initialized.current = true; } }, [account?.currency, setUseFiatAsMain]); + // update alt value + useEffect(() => { + (async () => { + if (showFiat) { + const { formattedSats, formattedFiat } = await convertValues( + Number(inputValue), + useFiatAsMain + ); + setAltFormattedValue(useFiatAsMain ? formattedSats : formattedFiat); + } + })(); + }, [useFiatAsMain, inputValue, convertValues, showFiat]); + const inputNode = ( Date: Sun, 14 Apr 2024 13:38:15 +0200 Subject: [PATCH 09/17] fix: input value --- src/app/components/form/DualCurrencyField/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/components/form/DualCurrencyField/index.tsx b/src/app/components/form/DualCurrencyField/index.tsx index e0d555f31b..4b8b3145a3 100644 --- a/src/app/components/form/DualCurrencyField/index.tsx +++ b/src/app/components/form/DualCurrencyField/index.tsx @@ -160,6 +160,7 @@ export default function DualCurrencyField({ await convertValues(value, useFiatAsMain); const wrappedEvent: DualCurrencyFieldChangeEvent = e as DualCurrencyFieldChangeEvent; + wrappedEvent.target.value = valueInSats.toString(); wrappedEvent.target.valueInFiat = valueInFiat; wrappedEvent.target.formattedValueInFiat = formattedFiat; wrappedEvent.target.valueInSats = valueInSats; From 278acaf2ccd64a12da4471e67ce649c6a73086b6 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Sun, 5 May 2024 11:22:03 +0200 Subject: [PATCH 10/17] fix: naming, empty input, do not change component to uncontrolled when value is empty --- .../form/DualCurrencyField/index.tsx | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/src/app/components/form/DualCurrencyField/index.tsx b/src/app/components/form/DualCurrencyField/index.tsx index 4b8b3145a3..835f2282ea 100644 --- a/src/app/components/form/DualCurrencyField/index.tsx +++ b/src/app/components/form/DualCurrencyField/index.tsx @@ -68,7 +68,7 @@ export default function DualCurrencyField({ const [altFormattedValue, setAltFormattedValue] = useState(""); const [minValue, setMinValue] = useState(min); const [maxValue, setMaxValue] = useState(max); - const [inputValue, setInputValue] = useState(value || 0); + const [inputValue, setInputValue] = useState(value); const [inputPrefix, setInputPrefix] = useState(""); const [inputPlaceHolder, setInputPlaceHolder] = useState(placeholder || ""); @@ -101,34 +101,38 @@ export default function DualCurrencyField({ ); const setUseFiatAsMain = useCallback( - async (v: boolean) => { - if (!showFiat) v = false; + async (useFiatAsMain: boolean) => { + if (!showFiat) useFiatAsMain = false; const userCurrency = settings?.currency || "BTC"; const rate = await getCurrencyRate(); if (min) { setMinValue( - v ? (Math.round(Number(min) * rate * 100) / 100.0).toString() : min + useFiatAsMain + ? (Math.round(Number(min) * rate * 100) / 100.0).toString() + : min ); } if (max) { setMaxValue( - v ? (Math.round(Number(max) * rate * 100) / 100.0).toString() : max + useFiatAsMain + ? (Math.round(Number(max) * rate * 100) / 100.0).toString() + : max ); } - const newValue = v + const newValue = useFiatAsMain ? Math.round(Number(inputValue) * rate * 100) / 100.0 : Math.round(Number(inputValue) / rate); - _setUseFiatAsMain(v); + _setUseFiatAsMain(useFiatAsMain); setInputValue(newValue); - setInputPrefix(getCurrencySymbol(v ? userCurrency : "BTC")); + setInputPrefix(getCurrencySymbol(useFiatAsMain ? userCurrency : "BTC")); if (!placeholder) { setInputPlaceHolder( tCommon("amount_placeholder", { - currency: v ? userCurrency : "sats", + currency: useFiatAsMain ? userCurrency : "sats", }) ); } @@ -186,7 +190,7 @@ export default function DualCurrencyField({ (async () => { if (showFiat) { const { formattedSats, formattedFiat } = await convertValues( - Number(inputValue), + Number(inputValue || 0), useFiatAsMain ); setAltFormattedValue(useFiatAsMain ? formattedSats : formattedFiat); @@ -212,7 +216,7 @@ export default function DualCurrencyField({ onChange={onChangeWrapper} onFocus={onFocus} onBlur={onBlur} - value={inputValue ? inputValue : undefined} + value={inputValue ? inputValue : ""} autoFocus={autoFocus} autoComplete={autoComplete} disabled={disabled} From 49bb31c10b3d5c81d37b89e89ad931743b55bf5a Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Fri, 24 May 2024 17:14:52 +0200 Subject: [PATCH 11/17] fix: fixes and improvements --- .../components/form/DualCurrencyField/index.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/app/components/form/DualCurrencyField/index.tsx b/src/app/components/form/DualCurrencyField/index.tsx index 835f2282ea..46752e4172 100644 --- a/src/app/components/form/DualCurrencyField/index.tsx +++ b/src/app/components/form/DualCurrencyField/index.tsx @@ -178,9 +178,7 @@ export default function DualCurrencyField({ // default to fiat when account currency is set to anything other than BTC useEffect(() => { if (!initialized.current) { - if (account?.currency && account?.currency !== "BTC") { - setUseFiatAsMain(true); - } + setUseFiatAsMain(!!(account?.currency && account?.currency !== "BTC")); initialized.current = true; } }, [account?.currency, setUseFiatAsMain]); @@ -271,7 +269,10 @@ export default function DualCurrencyField({ )} > {!!inputPrefix && ( -

+

{inputPrefix}

)} @@ -280,10 +281,11 @@ export default function DualCurrencyField({ {!!altFormattedValue && (

- ~{altFormattedValue} + {!useFiatAsMain && "~"} + {altFormattedValue}

)} From fd36df65d24dbcb9320fdde56e6efc83307226ff Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Sat, 25 May 2024 10:05:04 +0200 Subject: [PATCH 12/17] fix: light theme --- src/app/components/form/DualCurrencyField/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/components/form/DualCurrencyField/index.tsx b/src/app/components/form/DualCurrencyField/index.tsx index 46752e4172..0214a52e96 100644 --- a/src/app/components/form/DualCurrencyField/index.tsx +++ b/src/app/components/form/DualCurrencyField/index.tsx @@ -270,7 +270,7 @@ export default function DualCurrencyField({ > {!!inputPrefix && (

{inputPrefix} @@ -281,7 +281,7 @@ export default function DualCurrencyField({ {!!altFormattedValue && (

{!useFiatAsMain && "~"} From 5b75f436bddde88af68d4383bbbb8b5b1b736cf6 Mon Sep 17 00:00:00 2001 From: pavanjoshi914 Date: Thu, 30 May 2024 18:29:25 +0530 Subject: [PATCH 13/17] fix: dualcurrency component test --- src/app/components/form/DualCurrencyField/index.test.tsx | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/app/components/form/DualCurrencyField/index.test.tsx b/src/app/components/form/DualCurrencyField/index.test.tsx index ca4a748b22..ccef6a5f9c 100644 --- a/src/app/components/form/DualCurrencyField/index.test.tsx +++ b/src/app/components/form/DualCurrencyField/index.test.tsx @@ -1,4 +1,4 @@ -import { render, screen } from "@testing-library/react"; +import { render, screen, waitFor } from "@testing-library/react"; import { MemoryRouter } from "react-router-dom"; import { settingsFixture as mockSettings } from "~/../tests/fixtures/settings"; @@ -34,6 +34,9 @@ describe("DualCurrencyField", () => { const input = screen.getByLabelText("Amount"); expect(input).toBeInTheDocument(); - expect(await screen.getByText("~$10.00")).toBeInTheDocument(); + + await waitFor(() => { + expect(screen.getByText("~$10.00")).toBeInTheDocument(); + }); }); }); From 9ff833764626f075f7193f10080ecf83b08bc719 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Wed, 19 Jun 2024 14:57:07 +0200 Subject: [PATCH 14/17] fix: clone target to avoid side-effects when changing value --- .../form/DualCurrencyField/index.tsx | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/app/components/form/DualCurrencyField/index.tsx b/src/app/components/form/DualCurrencyField/index.tsx index 0214a52e96..8f3c8c4413 100644 --- a/src/app/components/form/DualCurrencyField/index.tsx +++ b/src/app/components/form/DualCurrencyField/index.tsx @@ -5,14 +5,16 @@ import { useSettings } from "~/app/context/SettingsContext"; import { classNames } from "~/app/utils"; import { RangeLabel } from "./rangeLabel"; +export type DualCurrencyFieldChangeEventTarget = HTMLInputElement & { + valueInFiat: number; + formattedValueInFiat: string; + valueInSats: number; + formattedValueInSats: string; +}; + export type DualCurrencyFieldChangeEvent = React.ChangeEvent & { - target: HTMLInputElement & { - valueInFiat: number; - formattedValueInFiat: string; - valueInSats: number; - formattedValueInSats: string; - }; + target: DualCurrencyFieldChangeEventTarget; }; export type Props = { @@ -159,11 +161,14 @@ export default function DualCurrencyField({ setInputValue(e.target.value); if (onChange) { + const wrappedEvent: DualCurrencyFieldChangeEvent = + e as DualCurrencyFieldChangeEvent; + const value = Number(e.target.value); const { valueInSats, formattedSats, valueInFiat, formattedFiat } = await convertValues(value, useFiatAsMain); - const wrappedEvent: DualCurrencyFieldChangeEvent = - e as DualCurrencyFieldChangeEvent; + wrappedEvent.target = + e.target.cloneNode() as DualCurrencyFieldChangeEventTarget; wrappedEvent.target.value = valueInSats.toString(); wrappedEvent.target.valueInFiat = valueInFiat; wrappedEvent.target.formattedValueInFiat = formattedFiat; From 564f79eaae92fdca20740259b65459a9a5c808fc Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Mon, 1 Jul 2024 17:00:04 +0200 Subject: [PATCH 15/17] fix: tests and implementation --- .../components/PaymentSummary/index.test.tsx | 2 +- .../components/SitePreferences/index.test.tsx | 57 +++++++++++++++---- .../form/DualCurrencyField/index.tsx | 18 +++--- src/app/screens/ConfirmKeysend/index.test.tsx | 18 ++++-- src/app/screens/ConfirmPayment/index.test.tsx | 1 + src/app/screens/Keysend/index.test.tsx | 6 +- src/app/screens/Keysend/index.tsx | 2 +- src/app/screens/LNURLChannel/index.test.tsx | 2 +- src/app/screens/LNURLPay/index.test.tsx | 10 +--- src/app/screens/MakeInvoice/index.test.tsx | 2 +- src/app/screens/MakeInvoice/index.tsx | 2 +- 11 files changed, 80 insertions(+), 40 deletions(-) diff --git a/src/app/components/PaymentSummary/index.test.tsx b/src/app/components/PaymentSummary/index.test.tsx index ecac0672b8..2ec32ff9cd 100644 --- a/src/app/components/PaymentSummary/index.test.tsx +++ b/src/app/components/PaymentSummary/index.test.tsx @@ -12,7 +12,7 @@ jest.mock("~/common/lib/api", () => { return { ...original, getSettings: jest.fn(() => Promise.resolve(mockSettings)), - getCurrencyRate: jest.fn(() => Promise.resolve({ rate: 11 })), + getCurrencyRate: jest.fn(() => 11), }; }); diff --git a/src/app/components/SitePreferences/index.test.tsx b/src/app/components/SitePreferences/index.test.tsx index 3b316db003..a4198d8b38 100644 --- a/src/app/components/SitePreferences/index.test.tsx +++ b/src/app/components/SitePreferences/index.test.tsx @@ -6,19 +6,20 @@ import { settingsFixture as mockSettings } from "~/../tests/fixtures/settings"; import type { Props } from "./index"; import SitePreferences from "./index"; -const mockGetFiatValue = jest.fn(() => Promise.resolve("$1,22")); +const mockGetFormattedFiat = jest.fn(() => "$1,22"); +const mockGetFormattedInCurrency = jest.fn((v, curr) => v + " " + curr); jest.mock("~/app/context/SettingsContext", () => ({ useSettings: () => ({ settings: mockSettings, isLoading: false, updateSetting: jest.fn(), - getFormattedFiat: mockGetFiatValue, + getFormattedFiat: mockGetFormattedFiat, getFormattedNumber: jest.fn(), getFormattedSats: jest.fn(), getCurrencyRate: jest.fn(() => 1), getCurrencySymbol: jest.fn(() => "₿"), - getFormattedInCurrency: mockGetFiatValue, + getFormattedInCurrency: mockGetFormattedInCurrency, }), })); @@ -56,7 +57,7 @@ describe("SitePreferences", () => { await renderComponent(); - expect(mockGetFiatValue).not.toHaveBeenCalled(); + expect(mockGetFormattedFiat).not.toHaveBeenCalled(); const settingsButton = await screen.getByRole("button"); @@ -69,25 +70,59 @@ describe("SitePreferences", () => { name: "Save", }); + const checkDualInputValues = (values: Array<[number, string]>) => { + for (let i = 0; i < values.length; i++) { + expect(mockGetFormattedInCurrency).toHaveBeenNthCalledWith( + i + 1, + ...values[i] + ); + } + expect(mockGetFormattedInCurrency).toHaveBeenCalledTimes(values.length); + }; + + const checkDualInputValue = (v: number, n: number) => { + for (let i = 1; i <= n * 2; i += 2) { + expect(mockGetFormattedInCurrency).toHaveBeenNthCalledWith(i, v, "BTC"); + expect(mockGetFormattedInCurrency).toHaveBeenNthCalledWith( + i + 1, + v, + "USD" + ); + } + expect(mockGetFormattedInCurrency).toHaveBeenCalledTimes(n * 2); + }; + // update fiat value when modal is open - expect(mockGetFiatValue).toHaveBeenCalledWith( - defaultProps.allowance.totalBudget.toString() - ); - expect(mockGetFiatValue).toHaveBeenCalledTimes(1); + checkDualInputValue(defaultProps.allowance.totalBudget, 2); await act(async () => { await user.clear(screen.getByLabelText("One-click payments budget")); + mockGetFormattedInCurrency.mockClear(); await user.type( screen.getByLabelText("One-click payments budget"), "250" ); }); + // update fiat value expect(screen.getByLabelText("One-click payments budget")).toHaveValue(250); - // update fiat value - expect(mockGetFiatValue).toHaveBeenCalledWith("250"); - expect(mockGetFiatValue).toHaveBeenCalledTimes(4); // plus 3 times for each input value 2, 5, 0 + checkDualInputValues([ + [2, "BTC"], + [2, "USD"], + [2, "BTC"], + [2, "USD"], + [25, "BTC"], + [25, "USD"], + [25, "BTC"], + [25, "USD"], + [250, "BTC"], + [250, "USD"], + [250, "BTC"], + [250, "USD"], + [250, "BTC"], + [250, "USD"], + ]); await act(async () => { await user.click(saveButton); diff --git a/src/app/components/form/DualCurrencyField/index.tsx b/src/app/components/form/DualCurrencyField/index.tsx index 8f3c8c4413..0d97b2d6d3 100644 --- a/src/app/components/form/DualCurrencyField/index.tsx +++ b/src/app/components/form/DualCurrencyField/index.tsx @@ -103,7 +103,7 @@ export default function DualCurrencyField({ ); const setUseFiatAsMain = useCallback( - async (useFiatAsMain: boolean) => { + async (useFiatAsMain: boolean, recalculateValue: boolean = true) => { if (!showFiat) useFiatAsMain = false; const userCurrency = settings?.currency || "BTC"; const rate = await getCurrencyRate(); @@ -124,12 +124,13 @@ export default function DualCurrencyField({ ); } - const newValue = useFiatAsMain - ? Math.round(Number(inputValue) * rate * 100) / 100.0 - : Math.round(Number(inputValue) / rate); - _setUseFiatAsMain(useFiatAsMain); - setInputValue(newValue); + if (recalculateValue) { + const newValue = useFiatAsMain + ? Math.round(Number(inputValue) * rate * 100) / 100.0 + : Math.round(Number(inputValue) / rate); + setInputValue(newValue); + } setInputPrefix(getCurrencySymbol(useFiatAsMain ? userCurrency : "BTC")); if (!placeholder) { setInputPlaceHolder( @@ -183,7 +184,10 @@ export default function DualCurrencyField({ // default to fiat when account currency is set to anything other than BTC useEffect(() => { if (!initialized.current) { - setUseFiatAsMain(!!(account?.currency && account?.currency !== "BTC")); + const initializeFiatMain = !!( + account?.currency && account?.currency !== "BTC" + ); + setUseFiatAsMain(initializeFiatMain, initializeFiatMain); initialized.current = true; } }, [account?.currency, setUseFiatAsMain]); diff --git a/src/app/screens/ConfirmKeysend/index.test.tsx b/src/app/screens/ConfirmKeysend/index.test.tsx index 7befe8e43a..8c22264201 100644 --- a/src/app/screens/ConfirmKeysend/index.test.tsx +++ b/src/app/screens/ConfirmKeysend/index.test.tsx @@ -4,14 +4,16 @@ import { MemoryRouter } from "react-router-dom"; import { settingsFixture as mockSettings } from "~/../tests/fixtures/settings"; import type { OriginData } from "~/types"; +import { waitFor } from "@testing-library/react"; import ConfirmKeysend from "./index"; const mockGetFiatValue = jest .fn() - .mockImplementationOnce(() => Promise.resolve("$0.00")) - .mockImplementationOnce(() => Promise.resolve("$0.00")) - .mockImplementationOnce(() => Promise.resolve("$0.01")) - .mockImplementationOnce(() => Promise.resolve("$0.05")); + .mockImplementationOnce(() => "$0.00") + .mockImplementationOnce(() => "$0.01") + .mockImplementationOnce(() => "$0.05"); + +const getFormattedInCurrency = jest.fn((v, c) => "$0.05"); jest.mock("~/app/context/SettingsContext", () => ({ useSettings: () => ({ @@ -21,7 +23,9 @@ jest.mock("~/app/context/SettingsContext", () => ({ getFormattedNumber: jest.fn(), getFormattedSats: jest.fn(() => "21 sats"), getFormattedFiat: mockGetFiatValue, - getFormattedInCurrency: mockGetFiatValue, + getFormattedInCurrency: getFormattedInCurrency, + getCurrencyRate: jest.fn(() => 11), + getCurrencySymbol: jest.fn(() => "₿"), }), })); @@ -96,6 +100,8 @@ describe("ConfirmKeysend", () => { const input = await screen.findByLabelText("Budget"); expect(input).toHaveValue(amount * 10); - expect(screen.getByText("~$0.05")).toBeInTheDocument(); + await waitFor(() => { + expect(screen.getByText("~$0.05")).toBeInTheDocument(); + }); }); }); diff --git a/src/app/screens/ConfirmPayment/index.test.tsx b/src/app/screens/ConfirmPayment/index.test.tsx index 77044f944d..30314360b2 100644 --- a/src/app/screens/ConfirmPayment/index.test.tsx +++ b/src/app/screens/ConfirmPayment/index.test.tsx @@ -44,6 +44,7 @@ jest.mock("~/app/context/SettingsContext", () => ({ useSettings: () => ({ settings: mockSettingsTmp, isLoading: false, + getCurrencyRate: jest.fn(() => 11), updateSetting: jest.fn(), getFormattedFiat: mockGetFiatValue, getFormattedNumber: jest.fn(), diff --git a/src/app/screens/Keysend/index.test.tsx b/src/app/screens/Keysend/index.test.tsx index f5f9534732..573f009624 100644 --- a/src/app/screens/Keysend/index.test.tsx +++ b/src/app/screens/Keysend/index.test.tsx @@ -1,4 +1,4 @@ -import { render, screen, act } from "@testing-library/react"; +import { act, render, screen } from "@testing-library/react"; import { MemoryRouter } from "react-router-dom"; import { settingsFixture as mockSettings } from "~/../tests/fixtures/settings"; import { SettingsProvider } from "~/app/context/SettingsContext"; @@ -42,7 +42,7 @@ jest.mock("~/common/lib/api", () => { return { ...original, getSettings: jest.fn(() => Promise.resolve(mockSettings)), - getCurrencyRate: jest.fn(() => Promise.resolve({ rate: 11 })), + getCurrencyRate: jest.fn(() => 11), }; }); @@ -59,6 +59,6 @@ describe("Keysend", () => { }); expect(await screen.findByText("Send payment to")).toBeInTheDocument(); - expect(await screen.getByLabelText("Amount (Satoshi)")).toHaveValue(21); + expect(await screen.getByLabelText("Amount")).toHaveValue(21); }); }); diff --git a/src/app/screens/Keysend/index.tsx b/src/app/screens/Keysend/index.tsx index 3e02645801..2a045edd4b 100644 --- a/src/app/screens/Keysend/index.tsx +++ b/src/app/screens/Keysend/index.tsx @@ -116,7 +116,7 @@ function Keysend() { /> { return { ...original, getSettings: jest.fn(() => Promise.resolve(mockSettings)), - getCurrencyRate: jest.fn(() => Promise.resolve({ rate: 11 })), + getCurrencyRate: jest.fn(() => 11), }; }); diff --git a/src/app/screens/LNURLPay/index.test.tsx b/src/app/screens/LNURLPay/index.test.tsx index 5ed28e25aa..ba287d1d74 100644 --- a/src/app/screens/LNURLPay/index.test.tsx +++ b/src/app/screens/LNURLPay/index.test.tsx @@ -1,11 +1,11 @@ -import { render, screen, waitFor } from "@testing-library/react"; +import { render, screen } from "@testing-library/react"; import { MemoryRouter } from "react-router-dom"; import { settingsFixture as mockSettings } from "~/../tests/fixtures/settings"; import type { LNURLDetails, OriginData } from "~/types"; import LNURLPay from "./index"; -const mockGetFiatValue = jest.fn(() => Promise.resolve("$1,22")); +const mockGetFiatValue = jest.fn(() => "$1,22"); jest.mock("~/app/context/SettingsContext", () => ({ useSettings: () => ({ @@ -96,12 +96,6 @@ describe("LNURLPay", () => { ); - // get fiat on mount - await waitFor(() => - expect(mockGetFiatValue).toHaveBeenCalledWith(satValue.toString()) - ); - await waitFor(() => expect(mockGetFiatValue).toHaveBeenCalledTimes(1)); - expect(await screen.getByText("blocktime 748949")).toBeInTheDocument(); expect(await screen.getByText("16sat/vB & empty")).toBeInTheDocument(); expect(await screen.getByLabelText("Amount")).toHaveValue(satValue); diff --git a/src/app/screens/MakeInvoice/index.test.tsx b/src/app/screens/MakeInvoice/index.test.tsx index 520a0cbb27..5dcde46ad0 100644 --- a/src/app/screens/MakeInvoice/index.test.tsx +++ b/src/app/screens/MakeInvoice/index.test.tsx @@ -64,7 +64,7 @@ describe("MakeInvoice", () => { ); }); - expect(await screen.findByLabelText("Amount (Satoshi)")).toHaveValue(21); + expect(await screen.findByLabelText("Amount")).toHaveValue(21); expect(await screen.findByLabelText("Memo")).toHaveValue("Test memo"); expect(screen.getByText(/~\$0.01/)).toBeInTheDocument(); }); diff --git a/src/app/screens/MakeInvoice/index.tsx b/src/app/screens/MakeInvoice/index.tsx index f1d7bb8e59..b8b5d1905c 100644 --- a/src/app/screens/MakeInvoice/index.tsx +++ b/src/app/screens/MakeInvoice/index.tsx @@ -113,7 +113,7 @@ function MakeInvoice() {

Date: Mon, 8 Jul 2024 08:17:03 +0200 Subject: [PATCH 16/17] fix: allow 4 decimals in fiat input and add some comments --- .../form/DualCurrencyField/index.tsx | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/app/components/form/DualCurrencyField/index.tsx b/src/app/components/form/DualCurrencyField/index.tsx index 0d97b2d6d3..aa73db43f6 100644 --- a/src/app/components/form/DualCurrencyField/index.tsx +++ b/src/app/components/form/DualCurrencyField/index.tsx @@ -6,10 +6,10 @@ import { classNames } from "~/app/utils"; import { RangeLabel } from "./rangeLabel"; export type DualCurrencyFieldChangeEventTarget = HTMLInputElement & { - valueInFiat: number; - formattedValueInFiat: string; - valueInSats: number; - formattedValueInSats: string; + valueInFiat: number; // current value converted to fiat + formattedValueInFiat: string; // current value in fiat formatted (e.g. $10.00) + valueInSats: number; // current value in sats + formattedValueInSats: string; // current value in sats formatted (e.g. 1000 sats) }; export type DualCurrencyFieldChangeEvent = @@ -24,8 +24,7 @@ export type Props = { hint?: string; amountExceeded?: boolean; rangeExceeded?: boolean; - baseToAltRate?: number; - showFiat?: boolean; + showFiat?: boolean; // compute and show fiat value onChange?: (e: DualCurrencyFieldChangeEvent) => void; }; @@ -74,6 +73,8 @@ export default function DualCurrencyField({ const [inputPrefix, setInputPrefix] = useState(""); const [inputPlaceHolder, setInputPlaceHolder] = useState(placeholder || ""); + // Perform currency conversions for the input value + // always returns formatted and raw values in sats and fiat const convertValues = useCallback( async (inputValue: number, inputInFiat: boolean) => { const userCurrency = settings?.currency || "BTC"; @@ -86,7 +87,7 @@ export default function DualCurrencyField({ valueInSats = Math.round(valueInFiat / rate); } else { valueInSats = Number(inputValue); - valueInFiat = Math.round(valueInSats * rate * 100) / 100.0; + valueInFiat = Math.round(valueInSats * rate * 10000) / 10000.0; } const formattedSats = getFormattedInCurrency(valueInSats, "BTC"); @@ -102,6 +103,7 @@ export default function DualCurrencyField({ [getCurrencyRate, getFormattedInCurrency, settings] ); + // Use fiat as main currency for the input const setUseFiatAsMain = useCallback( async (useFiatAsMain: boolean, recalculateValue: boolean = true) => { if (!showFiat) useFiatAsMain = false; @@ -111,7 +113,7 @@ export default function DualCurrencyField({ if (min) { setMinValue( useFiatAsMain - ? (Math.round(Number(min) * rate * 100) / 100.0).toString() + ? (Math.round(Number(min) * rate * 10000) / 10000.0).toString() : min ); } @@ -119,7 +121,7 @@ export default function DualCurrencyField({ if (max) { setMaxValue( useFiatAsMain - ? (Math.round(Number(max) * rate * 100) / 100.0).toString() + ? (Math.round(Number(max) * rate * 10000) / 10000.0).toString() : max ); } @@ -127,7 +129,7 @@ export default function DualCurrencyField({ _setUseFiatAsMain(useFiatAsMain); if (recalculateValue) { const newValue = useFiatAsMain - ? Math.round(Number(inputValue) * rate * 100) / 100.0 + ? Math.round(Number(inputValue) * rate * 10000) / 10000.0 : Math.round(Number(inputValue) / rate); setInputValue(newValue); } @@ -153,10 +155,12 @@ export default function DualCurrencyField({ ] ); + // helper to swap currencies (btc->fiat fiat->btc) const swapCurrencies = () => { setUseFiatAsMain(!useFiatAsMain); }; + // This wraps the onChange event and converts input values const onChangeWrapper = useCallback( async (e: React.ChangeEvent) => { setInputValue(e.target.value); @@ -165,16 +169,22 @@ export default function DualCurrencyField({ const wrappedEvent: DualCurrencyFieldChangeEvent = e as DualCurrencyFieldChangeEvent; + // Convert and inject the converted values into the event const value = Number(e.target.value); const { valueInSats, formattedSats, valueInFiat, formattedFiat } = await convertValues(value, useFiatAsMain); + + // we need to clone the target to avoid side effects on react internals wrappedEvent.target = e.target.cloneNode() as DualCurrencyFieldChangeEventTarget; + // ensure the value field is always in sats, this allows the code using this component + // to "reason in sats" and not have to worry about the user's currency wrappedEvent.target.value = valueInSats.toString(); wrappedEvent.target.valueInFiat = valueInFiat; wrappedEvent.target.formattedValueInFiat = formattedFiat; wrappedEvent.target.valueInSats = valueInSats; wrappedEvent.target.formattedValueInSats = formattedSats; + // Call the original onChange callback onChange(wrappedEvent); } }, @@ -229,7 +239,7 @@ export default function DualCurrencyField({ disabled={disabled} min={minValue} max={maxValue} - step={useFiatAsMain ? "0.01" : "1"} + step={useFiatAsMain ? "0.0001" : "1"} /> ); From c49b9258f090acca2bea4279fa480895e664db0a Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Sun, 14 Jul 2024 11:41:08 +0200 Subject: [PATCH 17/17] fix: preset workaround --- .../form/DualCurrencyField/index.tsx | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/app/components/form/DualCurrencyField/index.tsx b/src/app/components/form/DualCurrencyField/index.tsx index aa73db43f6..8dcc4ae047 100644 --- a/src/app/components/form/DualCurrencyField/index.tsx +++ b/src/app/components/form/DualCurrencyField/index.tsx @@ -72,6 +72,7 @@ export default function DualCurrencyField({ const [inputValue, setInputValue] = useState(value); const [inputPrefix, setInputPrefix] = useState(""); const [inputPlaceHolder, setInputPlaceHolder] = useState(placeholder || ""); + const [lastSeenInputValue, setLastSeenInputValue] = useState(value); // Perform currency conversions for the input value // always returns formatted and raw values in sats and fiat @@ -215,6 +216,40 @@ export default function DualCurrencyField({ })(); }, [useFiatAsMain, inputValue, convertValues, showFiat]); + // update input value when the value prop changes + useEffect(() => { + const newValue = Number(value || "0"); + const lastSeenValue = Number(lastSeenInputValue || "0"); + const currentValue = Number(inputValue || "0"); + const currentValueIsFiat = useFiatAsMain; + (async (newValue, lastSeenValue, currentValue, currentValueIsFiat) => { + const { valueInSats } = await convertValues( + currentValue, + currentValueIsFiat + ); + currentValue = Number(valueInSats); + // if the new value is different than the last seen value, it means it value was changes externally + if (newValue != lastSeenValue) { + // update the last seen value + setLastSeenInputValue(newValue.toString()); + // update input value unless the new value is equals to the current input value converted to sats + // (this means the external cose is passing the value from onChange to the input value) + if (newValue != currentValue) { + // Apply conversion for the input value + const { valueInSats, valueInFiat } = await convertValues( + Number(value), + false + ); + if (useFiatAsMain) { + setInputValue(valueInFiat); + } else { + setInputValue(valueInSats); + } + } + } + })(newValue, lastSeenValue, currentValue, currentValueIsFiat); + }, [value, lastSeenInputValue, inputValue, convertValues, useFiatAsMain]); + const inputNode = (