From 8c1567bc6f11b7d337b092ab9f3b40bddd495111 Mon Sep 17 00:00:00 2001 From: Riccardo Balbo Date: Sat, 16 Mar 2024 15:36:58 +0100 Subject: [PATCH 01/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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/28] 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 = ( Date: Sun, 8 Jun 2025 21:20:29 +0100 Subject: [PATCH 18/28] test(ConfirmPayment): mock getFormattedInCurrency to fix tests --- src/app/screens/ConfirmPayment/index.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/screens/ConfirmPayment/index.test.tsx b/src/app/screens/ConfirmPayment/index.test.tsx index 30314360b2..ac9d88ddd1 100644 --- a/src/app/screens/ConfirmPayment/index.test.tsx +++ b/src/app/screens/ConfirmPayment/index.test.tsx @@ -50,6 +50,7 @@ jest.mock("~/app/context/SettingsContext", () => ({ getFormattedNumber: jest.fn(), getFormattedSats: jest.fn(() => "25 sats"), getCurrencySymbol: jest.fn(() => "₿"), + getFormattedInCurrency: jest.fn(() => "$10.00"), }), })); From 94a527f85ac4b575dd28e79f290f88abd3a46103 Mon Sep 17 00:00:00 2001 From: Dunsin Date: Wed, 11 Jun 2025 06:56:31 +0100 Subject: [PATCH 19/28] fix: reduce unnecessary re-renders in input field and update test file --- .../components/SitePreferences/index.test.tsx | 9 ++- .../form/DualCurrencyField/index.tsx | 59 ++++++++----------- 2 files changed, 33 insertions(+), 35 deletions(-) diff --git a/src/app/components/SitePreferences/index.test.tsx b/src/app/components/SitePreferences/index.test.tsx index a4198d8b38..f52684e0f6 100644 --- a/src/app/components/SitePreferences/index.test.tsx +++ b/src/app/components/SitePreferences/index.test.tsx @@ -93,7 +93,7 @@ describe("SitePreferences", () => { }; // update fiat value when modal is open - checkDualInputValue(defaultProps.allowance.totalBudget, 2); + checkDualInputValue(defaultProps.allowance.totalBudget, 1); await act(async () => { await user.clear(screen.getByLabelText("One-click payments budget")); @@ -104,7 +104,6 @@ describe("SitePreferences", () => { ); }); - // update fiat value expect(screen.getByLabelText("One-click payments budget")).toHaveValue(250); checkDualInputValues([ @@ -122,6 +121,12 @@ describe("SitePreferences", () => { [250, "USD"], [250, "BTC"], [250, "USD"], + [250, "BTC"], + [250, "USD"], + [250, "BTC"], + [250, "USD"], + [250, "BTC"], + [250, "USD"], ]); await act(async () => { diff --git a/src/app/components/form/DualCurrencyField/index.tsx b/src/app/components/form/DualCurrencyField/index.tsx index 8dcc4ae047..a3eba204c9 100644 --- a/src/app/components/form/DualCurrencyField/index.tsx +++ b/src/app/components/form/DualCurrencyField/index.tsx @@ -74,6 +74,17 @@ export default function DualCurrencyField({ const [inputPlaceHolder, setInputPlaceHolder] = useState(placeholder || ""); const [lastSeenInputValue, setLastSeenInputValue] = useState(value); + const latestRate = useRef(null); + + // Store the latest currency rate in a ref + // to avoid triggering re-renders while keeping it accessible in async logic. + useEffect(() => { + const fetchInitialRate = async () => { + latestRate.current = await getCurrencyRate(); + }; + fetchInitialRate(); + }, [getCurrencyRate]); + // Perform currency conversions for the input value // always returns formatted and raw values in sats and fiat const convertValues = useCallback( @@ -109,7 +120,8 @@ export default function DualCurrencyField({ async (useFiatAsMain: boolean, recalculateValue: boolean = true) => { if (!showFiat) useFiatAsMain = false; const userCurrency = settings?.currency || "BTC"; - const rate = await getCurrencyRate(); + const rate = latestRate.current ?? (await getCurrencyRate()); + latestRate.current = rate; if (min) { setMinValue( @@ -174,7 +186,6 @@ export default function DualCurrencyField({ 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; @@ -189,7 +200,7 @@ export default function DualCurrencyField({ onChange(wrappedEvent); } }, - [onChange, useFiatAsMain, convertValues] + [onChange, convertValues, useFiatAsMain] ); // default to fiat when account currency is set to anything other than BTC @@ -216,39 +227,21 @@ export default function DualCurrencyField({ })(); }, [useFiatAsMain, inputValue, convertValues, showFiat]); - // update input value when the value prop changes + // Syncs external `value` prop into the input field when changed, avoiding unnecessary updates. 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]); + + if (newValue === lastSeenValue) return; + setLastSeenInputValue(newValue.toString()); + + // Immediately run an async function to convert values. + (async () => { + const { valueInSats, valueInFiat } = await convertValues(newValue, false); + + setInputValue(useFiatAsMain ? valueInFiat : valueInSats); + })(); + }, [value, lastSeenInputValue, convertValues, useFiatAsMain]); const inputNode = ( Date: Wed, 11 Jun 2025 07:11:50 +0100 Subject: [PATCH 20/28] test: adjust expected call count for mockGetFormattedInCurrency to match optimized logic --- src/app/components/SitePreferences/index.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/components/SitePreferences/index.test.tsx b/src/app/components/SitePreferences/index.test.tsx index f52684e0f6..2a4b45a6ce 100644 --- a/src/app/components/SitePreferences/index.test.tsx +++ b/src/app/components/SitePreferences/index.test.tsx @@ -89,7 +89,7 @@ describe("SitePreferences", () => { "USD" ); } - expect(mockGetFormattedInCurrency).toHaveBeenCalledTimes(n * 2); + expect(mockGetFormattedInCurrency).toHaveBeenCalledTimes(2); }; // update fiat value when modal is open From e51f018a8a2063fcd6f2aefd41da08e2dec1e604 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 3 Jun 2025 14:02:08 +0200 Subject: [PATCH 21/28] Translated using Weblate (German) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (805 of 805 strings) Co-authored-by: BSN ∞/21M ₿ Translate-URL: https://hosted.weblate.org/projects/getalby-lightning-browser-extension/getalby-lightning-browser-extension/de/ Translation: getAlby - lightning-browser-extension/getAlby - lightning-browser-extension --- src/i18n/locales/de/translation.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/locales/de/translation.json b/src/i18n/locales/de/translation.json index 1e36f50464..45703c1883 100644 --- a/src/i18n/locales/de/translation.json +++ b/src/i18n/locales/de/translation.json @@ -341,7 +341,8 @@ "title": "Browser-Benachrichtigungen" }, "website_enhancements": { - "title": "Website-Verbesserungen" + "title": "Website-Verbesserungen", + "subtitle": "Trinkgeldverbesserungen für Websites, die Alby unterstützen." }, "title": "Einstellungen der Erweiterung", "change_password": { From 2e036b2aafcea9d9d7d494e04d74a461a62ac363 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 3 Jun 2025 14:02:09 +0200 Subject: [PATCH 22/28] Translated using Weblate (Spanish) Currently translated at 49.8% (401 of 805 strings) Co-authored-by: Claudio Pastorino Translate-URL: https://hosted.weblate.org/projects/getalby-lightning-browser-extension/getalby-lightning-browser-extension/es/ Translation: getAlby - lightning-browser-extension/getAlby - lightning-browser-extension --- src/i18n/locales/es/translation.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/locales/es/translation.json b/src/i18n/locales/es/translation.json index ef8e4d8ac5..ce541b3e03 100644 --- a/src/i18n/locales/es/translation.json +++ b/src/i18n/locales/es/translation.json @@ -22,7 +22,8 @@ "connection_error": "Error de Conexión", "review_connection_details": "Revise los detalles de su conexión.", "connection_taking_long": "Conectarse está llevando más tiempo de lo esperado... ¿Sus datos son correctos? ¿Se puede acceder a su nodo?", - "contact_support": "Si necesita ayuda, póngase en contacto con support@getalby.com" + "contact_support": "Si necesita ayuda, póngase en contacto con support@getalby.com", + "initializing": "Inicializando su billetera. Por favor espere, puede tomar un minuto..." }, "pin_extension": { "title": "Fija tu extensión Alby", From 12fe08bcc797c2e37374ad63299778c1128f1b6b Mon Sep 17 00:00:00 2001 From: pavanjoshi914 Date: Fri, 6 Jun 2025 14:51:42 +0530 Subject: [PATCH 23/28] feat: add metadata to transactions migrate to sdkv5 --- package.json | 2 +- .../TransactionsTable/TransactionModal.tsx | 62 ++++++++++++++++++- .../components/TransactionsTable/index.tsx | 48 +++++++++----- .../screens/SendToBitcoinAddress/index.tsx | 2 +- .../screens/connectors/ConnectAlby/index.tsx | 2 +- src/app/utils/index.ts | 11 +++- src/common/lib/api.ts | 2 +- .../background-script/connectors/alby.ts | 21 ++++--- .../connectors/connector.interface.ts | 8 ++- .../background-script/connectors/nwc.ts | 1 + src/i18n/locales/en/translation.json | 2 + src/types.ts | 3 +- yarn.lock | 34 +++++----- 13 files changed, 148 insertions(+), 50 deletions(-) diff --git a/package.json b/package.json index 2c470294a1..4dfaa36c19 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ }, "dependencies": { "@bitcoinerlab/secp256k1": "^1.1.1", - "@getalby/sdk": "^3.9.0", + "@getalby/sdk": "^5.1.0", "@headlessui/react": "^1.7.18", "@lightninglabs/lnc-web": "^0.3.1-alpha", "@noble/ciphers": "^0.5.1", diff --git a/src/app/components/TransactionsTable/TransactionModal.tsx b/src/app/components/TransactionsTable/TransactionModal.tsx index 900d73dbc1..4e76945be4 100644 --- a/src/app/components/TransactionsTable/TransactionModal.tsx +++ b/src/app/components/TransactionsTable/TransactionModal.tsx @@ -1,17 +1,20 @@ +import { Nip47TransactionMetadata } from "@getalby/sdk/dist/nwc"; import { PopiconsArrowDownSolid, PopiconsArrowUpSolid, PopiconsChevronBottomLine, PopiconsChevronTopLine, + PopiconsLinkLine, PopiconsXSolid, } from "@popicons/react"; import dayjs from "dayjs"; +import { nip19 } from "nostr-tools"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import Hyperlink from "~/app/components/Hyperlink"; import Modal from "~/app/components/Modal"; import { useSettings } from "~/app/context/SettingsContext"; -import { classNames } from "~/app/utils"; +import { classNames, safeNpubEncode } from "~/app/utils"; import { Transaction } from "~/types"; type Props = { @@ -44,6 +47,15 @@ export default function TransactionModal({ return [tx.type && "sent"].includes(tx.type) ? "outgoing" : "incoming"; } + const eventId = transaction.metadata?.nostr?.tags?.find( + (t) => t[0] === "e" + )?.[1]; + + const pubkey = transaction.metadata?.nostr?.pubkey; + const npub = pubkey ? safeNpubEncode(pubkey) : undefined; + + const metadata = transaction.metadata as Nip47TransactionMetadata; + return (
+ {metadata?.recipient_data?.identifier && ( + + )} + {metadata?.payer_data?.name && ( + + )} )} + {metadata?.comment && ( + + )} + + {transaction.metadata?.nostr && eventId && npub && ( + + + {npub} + + + + } + /> + )} {transaction.publisherLink && transaction.title && ( )} - {transaction.boostagram?.ts && ( + {!!transaction.boostagram?.ts && ( )} + + {transaction.metadata && ( + + )}
)} diff --git a/src/app/components/TransactionsTable/index.tsx b/src/app/components/TransactionsTable/index.tsx index d12a6b8043..348a1fe628 100644 --- a/src/app/components/TransactionsTable/index.tsx +++ b/src/app/components/TransactionsTable/index.tsx @@ -10,7 +10,7 @@ import { useTranslation } from "react-i18next"; import TransactionModal from "~/app/components/TransactionsTable/TransactionModal"; import { useSettings } from "~/app/context/SettingsContext"; -import { classNames } from "~/app/utils"; +import { classNames, safeNpubEncode } from "~/app/utils"; import { Transaction } from "~/types"; export type Props = { @@ -21,7 +21,6 @@ export type Props = { export default function TransactionsTable({ transactions, - noResultMsg, loading = false, }: Props) { const { getFormattedSats, getFormattedInCurrency } = useSettings(); @@ -54,6 +53,35 @@ export default function TransactionsTable({ <> {transactions?.map((tx) => { const type = getTransactionType(tx); + const typeStateText = + type == "incoming" + ? t("received") + : t( + tx.state === "settled" + ? "sent" + : tx.state === "pending" + ? "sending" + : tx.state === "failed" + ? "failed" + : "sent" + ); + + const payerName = tx.metadata?.payer_data?.name; + const pubkey = tx.metadata?.nostr?.pubkey; + const npub = pubkey ? safeNpubEncode(pubkey) : undefined; + + const from = payerName + ? `from ${payerName}` + : npub + ? `zap from ${npub.substring(0, 12)}...` + : undefined; + + const recipientIdentifier = tx.metadata?.recipient_data?.identifier; + const to = recipientIdentifier + ? `${ + tx.state === "failed" ? "payment " : "" + }to ${recipientIdentifier}` + : undefined; return (
- {tx.title || - tx.boostagram?.message || - (type == "incoming" - ? t("received") - : t( - tx.state === "settled" - ? "sent" - : tx.state === "pending" - ? "sending" - : tx.state === "failed" - ? "failed" - : "sent" - ))} + {tx.title || tx.boostagram?.message || typeStateText} + {from !== undefined && <> {from}} + {to !== undefined && <> {to}}

diff --git a/src/app/screens/SendToBitcoinAddress/index.tsx b/src/app/screens/SendToBitcoinAddress/index.tsx index 7dc46234cb..295ac2ca3e 100644 --- a/src/app/screens/SendToBitcoinAddress/index.tsx +++ b/src/app/screens/SendToBitcoinAddress/index.tsx @@ -5,7 +5,7 @@ import IconButton from "@components/IconButton"; import DualCurrencyField, { DualCurrencyFieldChangeEvent, } from "@components/form/DualCurrencyField"; -import { CreateSwapResponse } from "@getalby/sdk/dist/types"; +import { CreateSwapResponse } from "@getalby/sdk/dist/oauth/types"; import { PopiconsChevronLeftLine, PopiconsLinkExternalSolid, diff --git a/src/app/screens/connectors/ConnectAlby/index.tsx b/src/app/screens/connectors/ConnectAlby/index.tsx index a38fea9e6a..e9f3e3bb28 100644 --- a/src/app/screens/connectors/ConnectAlby/index.tsx +++ b/src/app/screens/connectors/ConnectAlby/index.tsx @@ -1,4 +1,4 @@ -import { GetAccountInformationResponse } from "@getalby/sdk/dist/types"; +import { GetAccountInformationResponse } from "@getalby/sdk/dist/oauth/types"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; diff --git a/src/app/utils/index.ts b/src/app/utils/index.ts index f4ef1d2914..9daffdfbd6 100644 --- a/src/app/utils/index.ts +++ b/src/app/utils/index.ts @@ -1,4 +1,5 @@ -import { GetAccountInformationResponse } from "@getalby/sdk/dist/types"; +import { GetAccountInformationResponse } from "@getalby/sdk/dist/oauth/types"; +import { nip19 } from "nostr-tools"; import { useSettings } from "~/app/context/SettingsContext"; import api from "~/common/lib/api"; import { BrowserType, Theme } from "~/types"; @@ -83,3 +84,11 @@ export function extractLightningTagData(url: string) { return url.replace(/^lightning:/i, ""); } } + +export function safeNpubEncode(hex: string): string | undefined { + try { + return nip19.npubEncode(hex); + } catch { + return undefined; + } +} diff --git a/src/common/lib/api.ts b/src/common/lib/api.ts index a33350d296..bede1b2191 100644 --- a/src/common/lib/api.ts +++ b/src/common/lib/api.ts @@ -2,7 +2,7 @@ import { CreateSwapParams, CreateSwapResponse, SwapInfoResponse, -} from "@getalby/sdk/dist/types"; +} from "@getalby/sdk/dist/oauth/types"; import { ACCOUNT_CURRENCIES } from "~/common/constants"; import { ConnectPeerArgs, diff --git a/src/extension/background-script/connectors/alby.ts b/src/extension/background-script/connectors/alby.ts index 8542b60a69..e9478bb52a 100644 --- a/src/extension/background-script/connectors/alby.ts +++ b/src/extension/background-script/connectors/alby.ts @@ -1,4 +1,4 @@ -import { auth, Client } from "@getalby/sdk"; +import { oauth } from "@getalby/sdk"; import { CreateSwapParams, CreateSwapResponse, @@ -6,7 +6,7 @@ import { RequestOptions, SwapInfoResponse, Token, -} from "@getalby/sdk/dist/types"; +} from "@getalby/sdk/dist/oauth/types"; import browser from "webextension-polyfill"; import { decryptData, encryptData } from "~/common/lib/crypto"; import { Account, GetAccountInformationResponses, OAuthToken } from "~/types"; @@ -40,8 +40,8 @@ interface Config { export default class Alby implements Connector { private account: Account; private config: Config; - private _client: Client | undefined; - private _authUser: auth.OAuth2User | undefined; + private _client: oauth.Client | undefined; + private _authUser: oauth.auth.OAuth2User | undefined; private _cache = new Map(); constructor(account: Account, config: Config) { @@ -52,7 +52,10 @@ export default class Alby implements Connector { async init() { try { this._authUser = await this.authorize(); - this._client = new Client(this._authUser, this._getRequestOptions()); + this._client = new oauth.Client( + this._authUser, + this._getRequestOptions() + ); } catch (error) { console.error("Failed to initialize alby connector", error); this._authUser = undefined; @@ -270,7 +273,7 @@ export default class Alby implements Connector { return result; } - private async authorize(): Promise { + private async authorize(): Promise { try { const clientId = process.env.ALBY_OAUTH_CLIENT_ID; const clientSecret = process.env.ALBY_OAUTH_CLIENT_SECRET; @@ -280,7 +283,7 @@ export default class Alby implements Connector { const redirectURL = "https://getalby.com/extension/connect"; - const authClient = new auth.OAuth2User({ + const authClient = new oauth.auth.OAuth2User({ request_options: this._getRequestOptions(), client_id: clientId, client_secret: clientSecret, @@ -330,7 +333,7 @@ export default class Alby implements Connector { const oAuthTab = await browser.tabs.create({ url: authUrl }); - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const handleTabUpdated = ( tabId: number, changeInfo: browser.Tabs.OnUpdatedChangeInfoType, @@ -385,7 +388,7 @@ export default class Alby implements Connector { return urlSearchParams.get("code"); } - private async _request(func: (client: Client) => T) { + private async _request(func: (client: oauth.Client) => T) { if (!this._authUser || !this._client) { throw new Error("Alby client was not initialized"); } diff --git a/src/extension/background-script/connectors/connector.interface.ts b/src/extension/background-script/connectors/connector.interface.ts index 4349e6b934..863d13450a 100644 --- a/src/extension/background-script/connectors/connector.interface.ts +++ b/src/extension/background-script/connectors/connector.interface.ts @@ -1,8 +1,9 @@ +import { Nip47TransactionMetadata } from "@getalby/sdk/dist/nwc"; import { CreateSwapParams, CreateSwapResponse, SwapInfoResponse, -} from "@getalby/sdk/dist/types"; +} from "@getalby/sdk/dist/oauth/types"; import { ACCOUNT_CURRENCIES } from "~/common/constants"; import { OAuthToken } from "~/types"; @@ -37,6 +38,11 @@ export interface ConnectorTransaction { displayAmount?: [number, ACCOUNT_CURRENCIES]; type: "received" | "sent"; state?: "settled" | "pending" | "failed"; + recipient_data?: { + identifier?: string; + }; + + metadata?: Nip47TransactionMetadata; } export interface MakeInvoiceArgs { diff --git a/src/extension/background-script/connectors/nwc.ts b/src/extension/background-script/connectors/nwc.ts index 51b5fbbb59..ba426291e2 100644 --- a/src/extension/background-script/connectors/nwc.ts +++ b/src/extension/background-script/connectors/nwc.ts @@ -107,6 +107,7 @@ class NWCConnector implements Connector { transaction.metadata?.["tlv_records"] as TLVRecord[] | undefined ), state: transaction.state, + metadata: transaction.metadata, }) ); return { diff --git a/src/i18n/locales/en/translation.json b/src/i18n/locales/en/translation.json index dc770559ed..e7372f45c0 100644 --- a/src/i18n/locales/en/translation.json +++ b/src/i18n/locales/en/translation.json @@ -1267,7 +1267,9 @@ "fee": "Fee", "preimage": "Preimage", "payment_hash": "Payment Hash", + "metadata": "Metadata", "received": "Received", + "nostr_zap": "Nostr Zap From", "sent": "Sent", "sending": "Sending", "failed": "Failed", diff --git a/src/types.ts b/src/types.ts index 740948170c..52a9c0d338 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,7 +1,7 @@ import { CreateSwapParams, GetAccountInformationResponse, -} from "@getalby/sdk/dist/types"; +} from "@getalby/sdk/dist/oauth/types"; import { PaymentRequestObject } from "bolt11-signet"; import { Runtime } from "webextension-polyfill"; import { ACCOUNT_CURRENCIES, CURRENCIES } from "~/common/constants"; @@ -790,6 +790,7 @@ export type Transaction = { value?: string; publisherLink?: string; // either the invoice URL if on PublisherSingleView, or the internal link to Publisher state?: "settled" | "pending" | "failed"; + metadata?: ConnectorTransaction["metadata"]; }; export interface DbPayment { diff --git a/yarn.lock b/yarn.lock index 9ecfbfd12a..fe6a99789a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -675,13 +675,18 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.52.0.tgz#78fe5f117840f69dc4a353adf9b9cd926353378c" integrity sha512-mjZVbpaeMZludF2fsWLD0Z9gCref1Tk4i9+wddjRvpUNqqcndPkBD09N/Mapey0b3jaXbLm2kICwFv2E64QinA== -"@getalby/sdk@^3.9.0": - version "3.9.0" - resolved "https://registry.yarnpkg.com/@getalby/sdk/-/sdk-3.9.0.tgz#4eb4dd9512fc41312c8a894b7f653360feb61eac" - integrity sha512-qgNXr4FsX0a+PPvWgb112Q8h1/ov31zVP4LjsDYr5+W0CkrRbW9pQnsHPycVPLB5H8k5WVRRNkxYBBoWIBAwyw== +"@getalby/lightning-tools@^5.1.2": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@getalby/lightning-tools/-/lightning-tools-5.2.0.tgz#5f71ce9b25c04adeb7e421e920d0a28dfe32e082" + integrity sha512-8kBvENBTMh541VjGKhw3I29+549/C02gLSh3AQaMfoMNSZaMxfQW+7dcMcc7vbFaCKEcEe18ST5bUveTRBuXCQ== + +"@getalby/sdk@^5.1.0": + version "5.1.0" + resolved "https://registry.yarnpkg.com/@getalby/sdk/-/sdk-5.1.0.tgz#5e54d99b7c9470425ddcca529a95718f451efe0d" + integrity sha512-0ijo4enzoxZinyhOMFlR4h3qTQ9I0Se+dBkefk0ja5zOcpi61ZqT86n0T+7u94l8SH6/poysFBObdtN61u+6tQ== dependencies: - emittery "^1.0.3" - nostr-tools "2.9.4" + "@getalby/lightning-tools" "^5.1.2" + nostr-tools "2.12.0" "@headlessui/react@^1.7.18": version "1.7.18" @@ -4264,11 +4269,6 @@ emittery@^0.13.1: resolved "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz" integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ== -emittery@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/emittery/-/emittery-1.0.3.tgz#c9d2a9c689870f15251bb13b31c67715c26d69ac" - integrity sha512-tJdCJitoy2lrC2ldJcqN4vkqJ00lT+tOWNT1hBJjO/3FDMJa5TTIiYGCKGkn/WfCyOzUMObeohbVTj00fhiLiA== - emoji-log@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/emoji-log/-/emoji-log-1.0.2.tgz" @@ -7375,10 +7375,10 @@ normalize-range@^0.1.2: resolved "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz" integrity "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI= sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==" -nostr-tools@2.9.4: - version "2.9.4" - resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-2.9.4.tgz#ec0e1faa95bf9e5fee30b36c842a270135f40183" - integrity sha512-Powumwkp+EWbdK1T8IsEX4daTLQhtWJvitfZ6OP2BdU1jJZvNlUp3SQB541UYw4uc9jgLbxZW6EZSdZoSfIygQ== +nostr-tools@2.12.0: + version "2.12.0" + resolved "https://registry.yarnpkg.com/nostr-tools/-/nostr-tools-2.12.0.tgz#09f270e32453611a85c3670ff86ae856f3cbd21a" + integrity sha512-pUWEb020gTvt1XZvTa8AKNIHWFapjsv2NKyk43Ez2nnvz6WSXsrTFE0XtkNLSRBjPn6EpxumKeNiVzLz74jNSA== dependencies: "@noble/ciphers" "^0.5.1" "@noble/curves" "1.2.0" @@ -7387,7 +7387,7 @@ nostr-tools@2.9.4: "@scure/bip32" "1.3.1" "@scure/bip39" "1.2.1" optionalDependencies: - nostr-wasm v0.1.0 + nostr-wasm "0.1.0" nostr-tools@^1.17.0: version "1.17.0" @@ -7401,7 +7401,7 @@ nostr-tools@^1.17.0: "@scure/bip32" "1.3.1" "@scure/bip39" "1.2.1" -nostr-wasm@v0.1.0: +nostr-wasm@0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/nostr-wasm/-/nostr-wasm-0.1.0.tgz#17af486745feb2b7dd29503fdd81613a24058d94" integrity sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA== From cfa4135dcd515a159aeda711bb66805718e6055e Mon Sep 17 00:00:00 2001 From: pavanjoshi914 Date: Tue, 10 Jun 2025 15:50:44 +0530 Subject: [PATCH 24/28] chore: type correct --- .../background-script/connectors/connector.interface.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/extension/background-script/connectors/connector.interface.ts b/src/extension/background-script/connectors/connector.interface.ts index 863d13450a..7b3a6c62e9 100644 --- a/src/extension/background-script/connectors/connector.interface.ts +++ b/src/extension/background-script/connectors/connector.interface.ts @@ -38,10 +38,6 @@ export interface ConnectorTransaction { displayAmount?: [number, ACCOUNT_CURRENCIES]; type: "received" | "sent"; state?: "settled" | "pending" | "failed"; - recipient_data?: { - identifier?: string; - }; - metadata?: Nip47TransactionMetadata; } From 1dc552846a8a5abc685ddb91d59fec45f10119be Mon Sep 17 00:00:00 2001 From: pavanjoshi914 Date: Wed, 11 Jun 2025 20:06:22 +0530 Subject: [PATCH 25/28] chore: variable name --- .../TransactionsTable/TransactionModal.tsx | 16 +++++++--------- src/app/components/TransactionsTable/index.tsx | 8 +++++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/app/components/TransactionsTable/TransactionModal.tsx b/src/app/components/TransactionsTable/TransactionModal.tsx index 4e76945be4..340379ce71 100644 --- a/src/app/components/TransactionsTable/TransactionModal.tsx +++ b/src/app/components/TransactionsTable/TransactionModal.tsx @@ -47,14 +47,12 @@ export default function TransactionModal({ return [tx.type && "sent"].includes(tx.type) ? "outgoing" : "incoming"; } - const eventId = transaction.metadata?.nostr?.tags?.find( - (t) => t[0] === "e" - )?.[1]; + const metadata = transaction.metadata as Nip47TransactionMetadata; - const pubkey = transaction.metadata?.nostr?.pubkey; - const npub = pubkey ? safeNpubEncode(pubkey) : undefined; + const eventId = metadata?.nostr?.tags?.find((t) => t[0] === "e")?.[1]; - const metadata = transaction.metadata as Nip47TransactionMetadata; + const pubkey = metadata?.nostr?.pubkey; + const npub = pubkey ? safeNpubEncode(pubkey) : undefined; return ( )} - {transaction.metadata?.nostr && eventId && npub && ( + {metadata?.nostr && eventId && npub && ( )} - {transaction.metadata && ( + {metadata && ( )}

diff --git a/src/app/components/TransactionsTable/index.tsx b/src/app/components/TransactionsTable/index.tsx index 348a1fe628..e39803eb09 100644 --- a/src/app/components/TransactionsTable/index.tsx +++ b/src/app/components/TransactionsTable/index.tsx @@ -1,4 +1,5 @@ import Loading from "@components/Loading"; +import { Nip47TransactionMetadata } from "@getalby/sdk/dist/nwc"; import { PopiconsArrowDownSolid, PopiconsArrowUpSolid, @@ -66,8 +67,9 @@ export default function TransactionsTable({ : "sent" ); - const payerName = tx.metadata?.payer_data?.name; - const pubkey = tx.metadata?.nostr?.pubkey; + const metadata = tx.metadata as Nip47TransactionMetadata; + const payerName = metadata?.payer_data?.name; + const pubkey = metadata?.nostr?.pubkey; const npub = pubkey ? safeNpubEncode(pubkey) : undefined; const from = payerName @@ -76,7 +78,7 @@ export default function TransactionsTable({ ? `zap from ${npub.substring(0, 12)}...` : undefined; - const recipientIdentifier = tx.metadata?.recipient_data?.identifier; + const recipientIdentifier = metadata?.recipient_data?.identifier; const to = recipientIdentifier ? `${ tx.state === "failed" ? "payment " : "" From 52a6f1bf177f1dd4307e9d524c493b0159072769 Mon Sep 17 00:00:00 2001 From: pavanjoshi914 Date: Thu, 12 Jun 2025 15:14:04 +0530 Subject: [PATCH 26/28] chore: new design --- src/app/components/TransactionsTable/index.tsx | 17 +++++++++++------ src/app/hooks/useTransactions.ts | 1 + 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/app/components/TransactionsTable/index.tsx b/src/app/components/TransactionsTable/index.tsx index e39803eb09..52748ca520 100644 --- a/src/app/components/TransactionsTable/index.tsx +++ b/src/app/components/TransactionsTable/index.tsx @@ -91,7 +91,7 @@ export default function TransactionsTable({ className="-mx-2 px-2 py-2 hover:bg-gray-100 dark:hover:bg-surface-02dp cursor-pointer rounded-md" onClick={() => openDetails(tx)} > -
+
{type == "outgoing" ? ( tx.state === "pending" ? ( @@ -114,21 +114,26 @@ export default function TransactionsTable({ )}
-
+

- {tx.title || tx.boostagram?.message || typeStateText} + {typeStateText} {from !== undefined && <> {from}} {to !== undefined && <> {to}}

+

+ {tx.timeAgo} +

-

- {tx.timeAgo} -

+ {(tx.description || metadata?.comment) && ( +

+ {tx.description || metadata?.comment} +

+ )}
diff --git a/src/app/hooks/useTransactions.ts b/src/app/hooks/useTransactions.ts index c3839a294c..d23f2bd580 100644 --- a/src/app/hooks/useTransactions.ts +++ b/src/app/hooks/useTransactions.ts @@ -20,6 +20,7 @@ export const useTransactions = () => { (transaction) => ({ ...transaction, title: transaction.memo, + description: transaction.boostagram?.message || transaction.memo, timeAgo: dayjs( transaction.settleDate || transaction.creationDate ).fromNow(), From fe96c3500e8ba5c923b357d0cdf4d4fcab5b49a7 Mon Sep 17 00:00:00 2001 From: pavanjoshi914 Date: Thu, 12 Jun 2025 20:46:53 +0530 Subject: [PATCH 27/28] fix: tests --- src/app/components/TransactionsTable/index.test.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/components/TransactionsTable/index.test.tsx b/src/app/components/TransactionsTable/index.test.tsx index 8e8089de80..a88f64f87e 100644 --- a/src/app/components/TransactionsTable/index.test.tsx +++ b/src/app/components/TransactionsTable/index.test.tsx @@ -116,7 +116,7 @@ describe("TransactionsTable", () => { ); - expect(screen.getByText("Alby")).toBeInTheDocument(); + expect(screen.getByText("Sent")).toBeInTheDocument(); expect(screen.getByText(/5 days ago/)).toBeInTheDocument(); expect(await screen.findByText(/- 1,234,000 sats/)).toBeInTheDocument(); expect(await screen.findByText(/~\$241.02/)).toBeInTheDocument(); @@ -133,7 +133,7 @@ describe("TransactionsTable", () => { ); - expect(await screen.findByText("lambo lambo")).toBeInTheDocument(); + expect(await screen.getAllByText("Received")[0]).toBeInTheDocument(); expect(await screen.findByText(/4 days ago/)).toBeInTheDocument(); expect(await screen.findByText(/\+ 66,666 sats/)).toBeInTheDocument(); expect(await screen.findByText(/~\$13.02/)).toBeInTheDocument(); @@ -153,7 +153,7 @@ describe("TransactionsTable", () => { ); - expect(screen.getByText("dumplings")).toBeInTheDocument(); + expect(screen.getAllByText("Received")[0]).toBeInTheDocument(); expect(screen.getByText(/5 days ago/)).toBeInTheDocument(); expect(await screen.findByText(/\+ 88,888 sats/)).toBeInTheDocument(); expect(await screen.findByText(/~\$17.36/)).toBeInTheDocument(); From 48f40a29cb1fac51b12740fecc590c7f79507f79 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Sat, 14 Jun 2025 08:01:53 +0200 Subject: [PATCH 28/28] Translated using Weblate (German) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (807 of 807 strings) Co-authored-by: BSN ∞/21M ₿ Translate-URL: https://hosted.weblate.org/projects/getalby-lightning-browser-extension/getalby-lightning-browser-extension/de/ Translation: getAlby - lightning-browser-extension/getAlby - lightning-browser-extension --- src/i18n/locales/de/translation.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/locales/de/translation.json b/src/i18n/locales/de/translation.json index 45703c1883..42cd357a4b 100644 --- a/src/i18n/locales/de/translation.json +++ b/src/i18n/locales/de/translation.json @@ -1205,7 +1205,9 @@ "date_time": "Datum & Uhrzeit", "no_transactions": "Noch keine Transaktionen.", "failed": "Gescheitert", - "sending": "Senden von" + "sending": "Senden von", + "metadata": "Metadaten", + "nostr_zap": "Nostr Zap von" }, "budget_control": { "remember": {