Skip to content

Commit 8c1567b

Browse files
committed
feat: dual currency input supports fiat input
1 parent 5ea210a commit 8c1567b

File tree

13 files changed

+213
-148
lines changed

13 files changed

+213
-148
lines changed

src/app/components/BudgetControl/index.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,17 @@ type Props = {
99
onRememberChange: ChangeEventHandler<HTMLInputElement>;
1010
budget: string;
1111
onBudgetChange: ChangeEventHandler<HTMLInputElement>;
12-
fiatAmount: string;
1312
disabled?: boolean;
13+
showFiat?: boolean;
1414
};
1515

1616
function BudgetControl({
1717
remember,
1818
onRememberChange,
1919
budget,
2020
onBudgetChange,
21-
fiatAmount,
2221
disabled = false,
22+
showFiat = false,
2323
}: Props) {
2424
const { t } = useTranslation("components", {
2525
keyPrefix: "budget_control",
@@ -60,8 +60,8 @@ function BudgetControl({
6060

6161
<div>
6262
<DualCurrencyField
63+
showFiat={showFiat}
6364
autoFocus
64-
fiatValue={fiatAmount}
6565
id="budget"
6666
min={0}
6767
label={t("budget.label")}

src/app/components/SitePreferences/index.tsx

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,12 @@ export type Props = {
2525
};
2626

2727
function SitePreferences({ launcherType, allowance, onEdit, onDelete }: Props) {
28-
const {
29-
isLoading: isLoadingSettings,
30-
settings,
31-
getFormattedFiat,
32-
} = useSettings();
28+
const { isLoading: isLoadingSettings, settings } = useSettings();
3329
const showFiat = !isLoadingSettings && settings.showFiat;
3430
const { account } = useAccount();
3531
const [modalIsOpen, setIsOpen] = useState(false);
3632
const [budget, setBudget] = useState("");
3733
const [lnurlAuth, setLnurlAuth] = useState(false);
38-
const [fiatAmount, setFiatAmount] = useState("");
3934

4035
const [originalPermissions, setOriginalPermissions] = useState<
4136
Permission[] | null
@@ -79,17 +74,6 @@ function SitePreferences({ launcherType, allowance, onEdit, onDelete }: Props) {
7974
fetchPermissions();
8075
}, [account?.id, allowance.id]);
8176

82-
useEffect(() => {
83-
if (budget !== "" && showFiat) {
84-
const getFiat = async () => {
85-
const res = await getFormattedFiat(budget);
86-
setFiatAmount(res);
87-
};
88-
89-
getFiat();
90-
}
91-
}, [budget, showFiat, getFormattedFiat]);
92-
9377
function openModal() {
9478
setBudget(allowance.totalBudget.toString());
9579
setLnurlAuth(allowance.lnurlAuth);
@@ -196,7 +180,7 @@ function SitePreferences({ launcherType, allowance, onEdit, onDelete }: Props) {
196180
placeholder={tCommon("sats", { count: 0 })}
197181
value={budget}
198182
hint={t("hint")}
199-
fiatValue={fiatAmount}
183+
showFiat={showFiat}
200184
onChange={(e) => setBudget(e.target.value)}
201185
/>
202186
</div>

src/app/components/form/DualCurrencyField/index.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type { Props } from "./index";
55
import DualCurrencyField from "./index";
66

77
const props: Props = {
8-
fiatValue: "$10.00",
8+
showFiat: true,
99
label: "Amount",
1010
};
1111

src/app/components/form/DualCurrencyField/index.tsx

Lines changed: 158 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,35 @@
1-
import { useEffect, useRef } from "react";
1+
import { useCallback, useEffect, useRef, useState } from "react";
22
import { useTranslation } from "react-i18next";
3+
import { useAccount } from "~/app/context/AccountContext";
4+
import { useSettings } from "~/app/context/SettingsContext";
35
import { classNames } from "~/app/utils";
4-
56
import { RangeLabel } from "./rangeLabel";
67

8+
export type DualCurrencyFieldChangeEvent =
9+
React.ChangeEvent<HTMLInputElement> & {
10+
target: HTMLInputElement & {
11+
valueInFiat: number;
12+
formattedValueInFiat: string;
13+
valueInSats: number;
14+
formattedValueInSats: string;
15+
};
16+
};
17+
718
export type Props = {
819
suffix?: string;
920
endAdornment?: React.ReactNode;
10-
fiatValue: string;
1121
label: string;
1222
hint?: string;
1323
amountExceeded?: boolean;
1424
rangeExceeded?: boolean;
25+
baseToAltRate?: number;
26+
showFiat?: boolean;
27+
onChange?: (e: DualCurrencyFieldChangeEvent) => void;
1528
};
1629

1730
export default function DualCurrencyField({
1831
label,
19-
fiatValue,
32+
showFiat = true,
2033
id,
2134
placeholder,
2235
required = false,
@@ -38,10 +51,140 @@ export default function DualCurrencyField({
3851
rangeExceeded,
3952
}: React.InputHTMLAttributes<HTMLInputElement> & Props) {
4053
const { t: tCommon } = useTranslation("common");
54+
const { getFormattedInCurrency, getCurrencyRate, settings } = useSettings();
55+
const { account } = useAccount();
56+
4157
const inputEl = useRef<HTMLInputElement>(null);
4258
const outerStyles =
4359
"rounded-md border border-gray-300 dark:border-gray-800 bg-white dark:bg-black transition duration-300";
4460

61+
const initialized = useRef(false);
62+
const [useFiatAsMain, _setUseFiatAsMain] = useState(false);
63+
const [altFormattedValue, setAltFormattedValue] = useState("");
64+
const [minValue, setMinValue] = useState(min);
65+
const [maxValue, setMaxValue] = useState(max);
66+
const [inputValue, setInputValue] = useState(value || 0);
67+
68+
const getValues = useCallback(
69+
async (value: number, useFiatAsMain: boolean) => {
70+
let valueInSats = Number(value);
71+
let valueInFiat = 0;
72+
73+
if (showFiat) {
74+
valueInFiat = Number(value);
75+
const rate = await getCurrencyRate();
76+
if (useFiatAsMain) {
77+
valueInSats = Math.round(valueInSats / rate);
78+
} else {
79+
valueInFiat = Math.round(valueInFiat * rate * 100) / 100.0;
80+
}
81+
}
82+
83+
const formattedSats = getFormattedInCurrency(valueInSats, "BTC");
84+
let formattedFiat = "";
85+
86+
if (showFiat && valueInFiat) {
87+
formattedFiat = getFormattedInCurrency(valueInFiat, settings.currency);
88+
}
89+
90+
return {
91+
valueInSats,
92+
formattedSats,
93+
valueInFiat,
94+
formattedFiat,
95+
};
96+
},
97+
[getCurrencyRate, getFormattedInCurrency, showFiat, settings.currency]
98+
);
99+
100+
useEffect(() => {
101+
(async () => {
102+
if (showFiat) {
103+
const { formattedSats, formattedFiat } = await getValues(
104+
Number(inputValue),
105+
useFiatAsMain
106+
);
107+
setAltFormattedValue(useFiatAsMain ? formattedSats : formattedFiat);
108+
}
109+
})();
110+
}, [useFiatAsMain, inputValue, getValues, showFiat]);
111+
112+
const setUseFiatAsMain = useCallback(
113+
async (v: boolean) => {
114+
if (!showFiat) v = false;
115+
116+
const rate = showFiat ? await getCurrencyRate() : 1;
117+
if (min) {
118+
let minV;
119+
if (v) {
120+
minV = (Math.round(Number(min) * rate * 100) / 100.0).toString();
121+
} else {
122+
minV = min;
123+
}
124+
125+
setMinValue(minV);
126+
}
127+
if (max) {
128+
let maxV;
129+
if (v) {
130+
maxV = (Math.round(Number(max) * rate * 100) / 100.0).toString();
131+
} else {
132+
maxV = max;
133+
}
134+
135+
setMaxValue(maxV);
136+
}
137+
138+
let newValue;
139+
if (v) {
140+
newValue = Math.round(Number(inputValue) * rate * 100) / 100.0;
141+
} else {
142+
newValue = Math.round(Number(inputValue) / rate);
143+
}
144+
145+
_setUseFiatAsMain(v);
146+
setInputValue(newValue);
147+
},
148+
[showFiat, getCurrencyRate, inputValue, min, max]
149+
);
150+
151+
const swapCurrencies = () => {
152+
setUseFiatAsMain(!useFiatAsMain);
153+
};
154+
155+
const onChangeWrapper = useCallback(
156+
async (e: React.ChangeEvent<HTMLInputElement>) => {
157+
setInputValue(e.target.value);
158+
159+
if (onChange) {
160+
const value = Number(e.target.value);
161+
const { valueInSats, formattedSats, valueInFiat, formattedFiat } =
162+
await getValues(value, useFiatAsMain);
163+
const newEvent: DualCurrencyFieldChangeEvent = {
164+
...e,
165+
target: {
166+
...e.target,
167+
value: valueInSats.toString(),
168+
valueInFiat,
169+
formattedValueInFiat: formattedFiat,
170+
valueInSats,
171+
formattedValueInSats: formattedSats,
172+
},
173+
};
174+
onChange(newEvent);
175+
}
176+
},
177+
[onChange, useFiatAsMain, getValues]
178+
);
179+
180+
// default to fiat when account currency is set to anything other than BTC
181+
useEffect(() => {
182+
if (!initialized.current) {
183+
setUseFiatAsMain(!!(account?.currency && account?.currency !== "BTC"));
184+
initialized.current = true;
185+
}
186+
}, [account?.currency, setUseFiatAsMain]);
187+
45188
const inputNode = (
46189
<input
47190
ref={inputEl}
@@ -57,15 +200,16 @@ export default function DualCurrencyField({
57200
required={required}
58201
pattern={pattern}
59202
title={title}
60-
onChange={onChange}
203+
onChange={onChangeWrapper}
61204
onFocus={onFocus}
62205
onBlur={onBlur}
63-
value={value}
206+
value={inputValue}
64207
autoFocus={autoFocus}
65208
autoComplete={autoComplete}
66209
disabled={disabled}
67-
min={min}
68-
max={max}
210+
min={minValue}
211+
max={maxValue}
212+
step={useFiatAsMain ? "0.01" : "1"}
69213
/>
70214
);
71215

@@ -90,14 +234,15 @@ export default function DualCurrencyField({
90234
>
91235
{label}
92236
</label>
93-
{(min || max) && (
237+
{(minValue || maxValue) && (
94238
<span
95239
className={classNames(
96240
"text-xs text-gray-700 dark:text-neutral-400",
97241
!!rangeExceeded && "text-red-500 dark:text-red-500"
98242
)}
99243
>
100-
<RangeLabel min={min} max={max} /> {tCommon("sats_other")}
244+
<RangeLabel min={minValue} max={maxValue} />{" "}
245+
{useFiatAsMain ? "" : tCommon("sats_other")}
101246
</span>
102247
)}
103248
</div>
@@ -114,9 +259,9 @@ export default function DualCurrencyField({
114259
>
115260
{inputNode}
116261

117-
{!!fiatValue && (
118-
<p className="helper text-gray-500 z-1 pointer-events-none">
119-
~{fiatValue}
262+
{!!altFormattedValue && (
263+
<p className="helper text-gray-500 z-1" onClick={swapCurrencies}>
264+
~{altFormattedValue}
120265
</p>
121266
)}
122267

src/app/context/SettingsContext.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ interface SettingsContextType {
2323
getFormattedNumber: (amount: number | string) => string;
2424
getFormattedInCurrency: (
2525
amount: number | string,
26-
currency?: ACCOUNT_CURRENCIES
26+
currency?: ACCOUNT_CURRENCIES | CURRENCIES
2727
) => string;
28+
getCurrencyRate: () => Promise<number>;
2829
}
2930

3031
type Setting = Partial<SettingsStorage>;
@@ -115,7 +116,7 @@ export const SettingsProvider = ({
115116

116117
const getFormattedInCurrency = (
117118
amount: number | string,
118-
currency = "BTC" as ACCOUNT_CURRENCIES
119+
currency = "BTC" as ACCOUNT_CURRENCIES | CURRENCIES
119120
) => {
120121
if (currency === "BTC") {
121122
return getFormattedSats(amount);
@@ -149,6 +150,7 @@ export const SettingsProvider = ({
149150
getFormattedSats,
150151
getFormattedNumber,
151152
getFormattedInCurrency,
153+
getCurrencyRate,
152154
settings,
153155
updateSetting,
154156
isLoading,

src/app/screens/ConfirmKeysend/index.tsx

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ function ConfirmKeysend() {
4141
((parseInt(amount) || 0) * 10).toString()
4242
);
4343
const [fiatAmount, setFiatAmount] = useState("");
44-
const [fiatBudgetAmount, setFiatBudgetAmount] = useState("");
4544
const [loading, setLoading] = useState(false);
4645
const [successMessage, setSuccessMessage] = useState("");
4746

@@ -54,13 +53,6 @@ function ConfirmKeysend() {
5453
})();
5554
}, [amount, showFiat, getFormattedFiat]);
5655

57-
useEffect(() => {
58-
(async () => {
59-
const res = await getFormattedFiat(budget);
60-
setFiatBudgetAmount(res);
61-
})();
62-
}, [budget, showFiat, getFormattedFiat]);
63-
6456
async function confirm() {
6557
if (rememberMe && budget) {
6658
await saveBudget();
@@ -153,7 +145,7 @@ function ConfirmKeysend() {
153145
</div>
154146
<div>
155147
<BudgetControl
156-
fiatAmount={fiatBudgetAmount}
148+
showFiat={showFiat}
157149
remember={rememberMe}
158150
onRememberChange={(event) => {
159151
setRememberMe(event.target.checked);

0 commit comments

Comments
 (0)