Skip to content

Commit

Permalink
feat: dual currency input supports fiat input
Browse files Browse the repository at this point in the history
  • Loading branch information
riccardobl committed Mar 16, 2024
1 parent 5ea210a commit 8c1567b
Show file tree
Hide file tree
Showing 13 changed files with 213 additions and 148 deletions.
6 changes: 3 additions & 3 deletions src/app/components/BudgetControl/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,17 @@ type Props = {
onRememberChange: ChangeEventHandler<HTMLInputElement>;
budget: string;
onBudgetChange: ChangeEventHandler<HTMLInputElement>;
fiatAmount: string;
disabled?: boolean;
showFiat?: boolean;
};

function BudgetControl({
remember,
onRememberChange,
budget,
onBudgetChange,
fiatAmount,
disabled = false,
showFiat = false,
}: Props) {
const { t } = useTranslation("components", {
keyPrefix: "budget_control",
Expand Down Expand Up @@ -60,8 +60,8 @@ function BudgetControl({

<div>
<DualCurrencyField
showFiat={showFiat}
autoFocus
fiatValue={fiatAmount}
id="budget"
min={0}
label={t("budget.label")}
Expand Down
20 changes: 2 additions & 18 deletions src/app/components/SitePreferences/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,12 @@ export type Props = {
};

function SitePreferences({ launcherType, allowance, onEdit, onDelete }: Props) {
const {
isLoading: isLoadingSettings,
settings,
getFormattedFiat,
} = useSettings();
const { isLoading: isLoadingSettings, settings } = useSettings();
const showFiat = !isLoadingSettings && settings.showFiat;
const { account } = useAccount();
const [modalIsOpen, setIsOpen] = useState(false);
const [budget, setBudget] = useState("");
const [lnurlAuth, setLnurlAuth] = useState(false);
const [fiatAmount, setFiatAmount] = useState("");

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

useEffect(() => {
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);
Expand Down Expand Up @@ -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)}
/>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/app/components/form/DualCurrencyField/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { Props } from "./index";
import DualCurrencyField from "./index";

const props: Props = {
fiatValue: "$10.00",
showFiat: true,
label: "Amount",
};

Expand Down
171 changes: 158 additions & 13 deletions src/app/components/form/DualCurrencyField/index.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLInputElement> & {
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,
Expand All @@ -38,10 +51,140 @@ export default function DualCurrencyField({
rangeExceeded,
}: React.InputHTMLAttributes<HTMLInputElement> & Props) {
const { t: tCommon } = useTranslation("common");
const { getFormattedInCurrency, getCurrencyRate, settings } = useSettings();
const { account } = useAccount();

const inputEl = useRef<HTMLInputElement>(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<HTMLInputElement>) => {
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 = (
<input
ref={inputEl}
Expand All @@ -57,15 +200,16 @@ export default function DualCurrencyField({
required={required}
pattern={pattern}
title={title}
onChange={onChange}
onChange={onChangeWrapper}
onFocus={onFocus}
onBlur={onBlur}
value={value}
value={inputValue}
autoFocus={autoFocus}
autoComplete={autoComplete}
disabled={disabled}
min={min}
max={max}
min={minValue}
max={maxValue}
step={useFiatAsMain ? "0.01" : "1"}
/>
);

Expand All @@ -90,14 +234,15 @@ export default function DualCurrencyField({
>
{label}
</label>
{(min || max) && (
{(minValue || maxValue) && (
<span
className={classNames(
"text-xs text-gray-700 dark:text-neutral-400",
!!rangeExceeded && "text-red-500 dark:text-red-500"
)}
>
<RangeLabel min={min} max={max} /> {tCommon("sats_other")}
<RangeLabel min={minValue} max={maxValue} />{" "}
{useFiatAsMain ? "" : tCommon("sats_other")}
</span>
)}
</div>
Expand All @@ -114,9 +259,9 @@ export default function DualCurrencyField({
>
{inputNode}

{!!fiatValue && (
<p className="helper text-gray-500 z-1 pointer-events-none">
~{fiatValue}
{!!altFormattedValue && (
<p className="helper text-gray-500 z-1" onClick={swapCurrencies}>
~{altFormattedValue}
</p>
)}

Expand Down
6 changes: 4 additions & 2 deletions src/app/context/SettingsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<number>;
}

type Setting = Partial<SettingsStorage>;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -149,6 +150,7 @@ export const SettingsProvider = ({
getFormattedSats,
getFormattedNumber,
getFormattedInCurrency,
getCurrencyRate,
settings,
updateSetting,
isLoading,
Expand Down
10 changes: 1 addition & 9 deletions src/app/screens/ConfirmKeysend/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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("");

Expand All @@ -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();
Expand Down Expand Up @@ -153,7 +145,7 @@ function ConfirmKeysend() {
</div>
<div>
<BudgetControl
fiatAmount={fiatBudgetAmount}
showFiat={showFiat}
remember={rememberMe}
onRememberChange={(event) => {
setRememberMe(event.target.checked);
Expand Down
Loading

0 comments on commit 8c1567b

Please sign in to comment.