-
Notifications
You must be signed in to change notification settings - Fork 196
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support fiat input in dual currency field #3089
base: master
Are you sure you want to change the base?
Changes from 14 commits
8c1567b
87a8edc
3bffba9
29c49b4
90512cf
3f7f107
96635c8
0394bb7
2eb1020
5a214c9
2e2e329
ea1954f
9cfa078
278acaf
49bb31c
fd36df6
269f8a3
5b75f43
9ff8337
564f79e
ba7cb69
0cdba7b
c49b925
8477fa7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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, | ||
|
@@ -38,10 +51,153 @@ export default function DualCurrencyField({ | |
rangeExceeded, | ||
}: React.InputHTMLAttributes<HTMLInputElement> & Props) { | ||
const { t: tCommon } = useTranslation("common"); | ||
const { | ||
getFormattedInCurrency, | ||
getCurrencyRate, | ||
getCurrencySymbol, | ||
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); | ||
const [inputPrefix, setInputPrefix] = useState(""); | ||
const [inputPlaceHolder, setInputPlaceHolder] = useState(placeholder || ""); | ||
|
||
const convertValues = useCallback( | ||
async (inputValue: number, inputInFiat: boolean) => { | ||
const userCurrency = settings?.currency || "BTC"; | ||
let valueInSats = 0; | ||
let valueInFiat = 0; | ||
const rate = await getCurrencyRate(); | ||
|
||
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 = getFormattedInCurrency(valueInFiat, userCurrency); | ||
|
||
return { | ||
valueInSats, | ||
formattedSats, | ||
valueInFiat, | ||
formattedFiat, | ||
}; | ||
}, | ||
[getCurrencyRate, getFormattedInCurrency, settings] | ||
); | ||
|
||
const setUseFiatAsMain = useCallback( | ||
async (useFiatAsMain: boolean) => { | ||
if (!showFiat) useFiatAsMain = false; | ||
const userCurrency = settings?.currency || "BTC"; | ||
const rate = await getCurrencyRate(); | ||
|
||
if (min) { | ||
setMinValue( | ||
useFiatAsMain | ||
? (Math.round(Number(min) * rate * 100) / 100.0).toString() | ||
: min | ||
); | ||
} | ||
|
||
if (max) { | ||
setMaxValue( | ||
useFiatAsMain | ||
? (Math.round(Number(max) * rate * 100) / 100.0).toString() | ||
: max | ||
); | ||
} | ||
|
||
const newValue = useFiatAsMain | ||
? Math.round(Number(inputValue) * rate * 100) / 100.0 | ||
: Math.round(Number(inputValue) / rate); | ||
|
||
_setUseFiatAsMain(useFiatAsMain); | ||
setInputValue(newValue); | ||
setInputPrefix(getCurrencySymbol(useFiatAsMain ? userCurrency : "BTC")); | ||
if (!placeholder) { | ||
setInputPlaceHolder( | ||
tCommon("amount_placeholder", { | ||
currency: useFiatAsMain ? userCurrency : "sats", | ||
}) | ||
); | ||
} | ||
}, | ||
[ | ||
settings, | ||
showFiat, | ||
getCurrencyRate, | ||
inputValue, | ||
min, | ||
max, | ||
tCommon, | ||
getCurrencySymbol, | ||
placeholder, | ||
] | ||
); | ||
|
||
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 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; | ||
wrappedEvent.target.formattedValueInSats = formattedSats; | ||
onChange(wrappedEvent); | ||
} | ||
}, | ||
[onChange, useFiatAsMain, convertValues] | ||
); | ||
|
||
// 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); | ||
} | ||
initialized.current = true; | ||
} | ||
}, [account?.currency, setUseFiatAsMain]); | ||
|
||
// update alt value | ||
useEffect(() => { | ||
(async () => { | ||
if (showFiat) { | ||
const { formattedSats, formattedFiat } = await convertValues( | ||
Number(inputValue || 0), | ||
useFiatAsMain | ||
); | ||
setAltFormattedValue(useFiatAsMain ? formattedSats : formattedFiat); | ||
} | ||
})(); | ||
}, [useFiatAsMain, inputValue, convertValues, showFiat]); | ||
|
||
const inputNode = ( | ||
<input | ||
ref={inputEl} | ||
|
@@ -53,19 +209,20 @@ 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={onChange} | ||
onChange={onChangeWrapper} | ||
onFocus={onFocus} | ||
onBlur={onBlur} | ||
value={value} | ||
value={inputValue ? inputValue : ""} | ||
autoFocus={autoFocus} | ||
autoComplete={autoComplete} | ||
disabled={disabled} | ||
min={min} | ||
max={max} | ||
min={minValue} | ||
max={maxValue} | ||
step={useFiatAsMain ? "0.01" : "1"} | ||
/> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what if we keep step value upto 4 decimal places. so that user can enter values for 10 20 sats in USD as well (it has no major use but just to keep it fully flexible) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
); | ||
|
||
|
@@ -90,14 +247,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> | ||
|
@@ -112,17 +270,26 @@ export default function DualCurrencyField({ | |
outerStyles | ||
)} | ||
> | ||
{!!inputPrefix && ( | ||
<p className="helper text-gray-500 z-1 pr-2" onClick={swapCurrencies}> | ||
{inputPrefix} | ||
</p> | ||
)} | ||
|
||
{inputNode} | ||
|
||
{!!fiatValue && ( | ||
<p className="helper text-gray-500 z-1 pointer-events-none"> | ||
~{fiatValue} | ||
{!!altFormattedValue && ( | ||
<p | ||
className="helper whitespace-nowrap text-gray-500 z-1" | ||
onClick={swapCurrencies} | ||
> | ||
~{altFormattedValue} | ||
</p> | ||
)} | ||
|
||
{suffix && ( | ||
<span | ||
className="flex items-center px-3 font-medium bg-white dark:bg-surface-00dp dark:text-white" | ||
className="flex items-center px-3 font-medium bg-white dark:bg-surface-00dp dark:text-white" | ||
onClick={() => { | ||
inputEl.current?.focus(); | ||
}} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now its a good enough complexity. maybe add comments wherere necessary why such and such thing is used
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added some comments to the code and removed baseToAltRate since it was dead code from one of the previous iterations 👍