From f0d08ad61d559075f182725bf4b44c807c62b282 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Fri, 15 Mar 2024 18:46:13 +0100 Subject: [PATCH] feat: support sat-fiat switch in dual currency 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 | 119 ++++++++++++++++-- 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 | 24 ++-- 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 | 27 ++-- 13 files changed, 169 insertions(+), 142 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..a9547fade7 100644 --- a/src/app/components/form/DualCurrencyField/index.tsx +++ b/src/app/components/form/DualCurrencyField/index.tsx @@ -1,28 +1,36 @@ -import { useEffect, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; +import { useSettings } from "~/app/context/SettingsContext"; import { classNames } from "~/app/utils"; - import { RangeLabel } from "./rangeLabel"; export type Props = { suffix?: string; endAdornment?: React.ReactNode; - fiatValue: string; label: string; hint?: string; amountExceeded?: boolean; rangeExceeded?: boolean; + baseToAltRate?: number; + showFiat?: boolean; + onValueChange?: ( + valueInSats: number, + formattedValueInSata: string, + valueInFiat: number, + formattedValueInFiat: string + ) => void; }; export default function DualCurrencyField({ label, - fiatValue, + showFiat = true, id, placeholder, required = false, pattern, title, onChange, + onValueChange, onFocus, onBlur, value, @@ -38,10 +46,95 @@ export default function DualCurrencyField({ rangeExceeded, }: React.InputHTMLAttributes & Props) { const { t: tCommon } = useTranslation("common"); + const { getFormattedInCurrency, getCurrencyRate, settings } = useSettings(); + 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 [useFiatAsMain, _setUseFiatAsMain] = useState(false); + + const [altFormattedValue, setAltFormattedValue] = useState(""); + const [minValue, setMinValue] = useState(min); + const [maxValue, setMaxValue] = useState(max); + const [inputValue, setInputValue] = useState(value); + + const refreshValue = async (value: number, useFiatAsMain: boolean) => { + setInputValue(value); + + let valueInSats = Number(value); + let valueInFiat; + + 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); + setAltFormattedValue(useFiatAsMain ? formattedSats : formattedFiat); + } + + return { + valueInSats, + formattedSats, + valueInFiat, + formattedFiat, + }; + }; + + const setUseFiatAsMain = async (v: boolean) => { + if (!showFiat) v = false; + + if (v === useFiatAsMain) return; + const rate = showFiat ? await getCurrencyRate() : 1; + if (min) { + setMinValue(Number(min) * rate); + } + if (max) { + setMaxValue(Number(max) * rate); + } + + let newValue; + if (v) { + newValue = Math.round(Number(inputValue) * rate * 100) / 100.0; + } else { + newValue = Math.round(Number(inputValue) / rate); + } + _setUseFiatAsMain(v); + refreshValue(newValue, v); + }; + + const swapCurrencies = () => { + setUseFiatAsMain(!useFiatAsMain); + }; + + const onChangeWrapper = async (e: React.ChangeEvent) => { + const value = e.target.value; + const { valueInSats, formattedSats, valueInFiat, formattedFiat } = + await refreshValue(Number(value), useFiatAsMain); + if (onValueChange) { + onValueChange( + valueInSats, + formattedSats, + valueInFiat || 0, + formattedFiat || "" + ); + } + if (onChange) { + e.target.value = valueInSats.toString(); + onChange(e); + } + }; + const inputNode = ( ); @@ -90,14 +183,14 @@ export default function DualCurrencyField({ > {label} - {(min || max) && ( + {(minValue || maxValue) && ( - {tCommon("sats_other")} + {tCommon("sats_other")} )} @@ -114,9 +207,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..75d32416e1 100644 --- a/src/app/screens/Keysend/index.tsx +++ b/src/app/screens/Keysend/index.tsx @@ -7,7 +7,7 @@ import ResultCard from "@components/ResultCard"; import SatButtons from "@components/SatButtons"; import DualCurrencyField 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 +21,6 @@ function Keysend() { const { isLoading: isLoadingSettings, settings, - getFormattedFiat, getFormattedSats, } = useSettings(); const showFiat = !isLoadingSettings && settings.showFiat; @@ -46,15 +45,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 +116,17 @@ function Keysend() { id="amount" label={t("amount.label")} min={1} - onChange={(e) => setAmountSat(e.target.value)} value={amountSat} - fiatValue={fiatAmount} + showFiat={showFiat} + onValueChange={( + valueInSats, + formattedValueInSats, + valueInFiat, + formattedValueInFiat + ) => { + setAmountSat(valueInSats.toString()); + setFiatAmount(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..8752ab44c7 100644 --- a/src/app/screens/LNURLPay/index.tsx +++ b/src/app/screens/LNURLPay/index.tsx @@ -53,7 +53,6 @@ function LNURLPay() { const { isLoading: isLoadingSettings, settings, - getFormattedFiat, getFormattedSats, } = useSettings(); const showFiat = !isLoadingSettings && settings.showFiat; @@ -87,15 +86,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 +441,16 @@ function LNURLPay() { max={amountMax} rangeExceeded={rangeExceeded} value={valueSat} - onChange={(e) => setValueSat(e.target.value)} - fiatValue={fiatValue} + onValueChange={( + valueInSats, + formattedValueInSats, + valueInFiat, + formattedValueInFiat + ) => { + setValueSat(valueInSats.toString()); + setFiatValue(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..bafad216fc 100644 --- a/src/app/screens/LNURLWithdraw/index.tsx +++ b/src/app/screens/LNURLWithdraw/index.tsx @@ -6,7 +6,7 @@ import PublisherCard from "@components/PublisherCard"; import ResultCard from "@components/ResultCard"; import DualCurrencyField 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"; @@ -27,7 +27,6 @@ function LNURLWithdraw() { const { isLoading: isLoadingSettings, settings, - getFormattedFiat, getFormattedSats, } = useSettings(); const showFiat = !isLoadingSettings && settings.showFiat; @@ -43,15 +42,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 +107,16 @@ function LNURLWithdraw() { min={Math.floor(minWithdrawable / 1000)} max={Math.floor(maxWithdrawable / 1000)} value={valueSat} - onChange={(e) => setValueSat(e.target.value)} - fiatValue={fiatValue} + onValueChange={( + valueInSats, + formattedValueInSat, + valueInFiat, + formattedValueInFiat + ) => { + setValueSat(valueInSats.toString()); + setFiatValue(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..aa753388c9 100644 --- a/src/app/screens/SendToBitcoinAddress/index.tsx +++ b/src/app/screens/SendToBitcoinAddress/index.tsx @@ -1,11 +1,13 @@ -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 { 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 +67,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 +248,17 @@ function SendToBitcoinAddress() { label={tCommon("amount")} min={amountMin} max={amountMax} - onChange={(e) => setAmountSat(e.target.value)} + onValueChange={( + valueInSats, + formattedValueInSats, + valueInFiat, + formattedValueInFiat + ) => { + setAmountSat(valueInSats.toString()); + setFiatAmount(formattedValueInFiat); + }} + showFiat={showFiat} value={amountSat} - fiatValue={fiatAmount} rangeExceeded={rangeExceeded} amountExceeded={amountExceeded} hint={`${tCommon("balance")}: ${auth?.balancesDecorated